tint: Optimize sem node lookup

Add a 'NodeID' to each ast::Node which is the sequentially allocated
index of the node. Use this in sem::Info to map the AST node to the
semantic node, instead of using a std::unordered_map.

Optimised very hot code by entirely eliminating map lookups, and
dramatically reducing cache misses (lookups are usually sequentually
ordered).

Timings running
'webgpu:shader,execution,expression,call,builtin,atan2:f32:inputSource="const";vectorize="_undef_"'
with dawn/node, using SwiftShader:

    Without change: 3.22647107s
    With change:    3.10578879s

Bug: tint:1613
Change-Id: I22ec48d933b2e5f9da04494bff4e979e6f7b1982
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/96140
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index c0c0b62..825741c 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -287,6 +287,7 @@
     "ast/module.h",
     "ast/multisampled_texture.cc",
     "ast/multisampled_texture.h",
+    "ast/node_id.h",
     "ast/node.cc",
     "ast/node.h",
     "ast/override.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 6c611c3..ec0e78e 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -157,6 +157,7 @@
   ast/module.h
   ast/multisampled_texture.cc
   ast/multisampled_texture.h
+  ast/node_id.h
   ast/node.cc
   ast/node.h
   ast/override.cc
diff --git a/src/tint/ast/alias.cc b/src/tint/ast/alias.cc
index fa98cd4..8a23e8f 100644
--- a/src/tint/ast/alias.cc
+++ b/src/tint/ast/alias.cc
@@ -20,8 +20,8 @@
 
 namespace tint::ast {
 
-Alias::Alias(ProgramID pid, const Source& src, const Symbol& n, const Type* subtype)
-    : Base(pid, src, n), type(subtype) {
+Alias::Alias(ProgramID pid, NodeID nid, const Source& src, const Symbol& n, const Type* subtype)
+    : Base(pid, nid, src, n), type(subtype) {
     TINT_ASSERT(AST, type);
 }
 
diff --git a/src/tint/ast/alias.h b/src/tint/ast/alias.h
index 87ce578..74d91b0 100644
--- a/src/tint/ast/alias.h
+++ b/src/tint/ast/alias.h
@@ -26,10 +26,11 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param name the symbol for the alias
     /// @param subtype the alias'd type
-    Alias(ProgramID pid, const Source& src, const Symbol& name, const Type* subtype);
+    Alias(ProgramID pid, NodeID nid, const Source& src, const Symbol& name, const Type* subtype);
     /// Move constructor
     Alias(Alias&&);
     /// Destructor
diff --git a/src/tint/ast/array.cc b/src/tint/ast/array.cc
index 0389ed0..cd1fc26 100644
--- a/src/tint/ast/array.cc
+++ b/src/tint/ast/array.cc
@@ -38,11 +38,12 @@
 }  // namespace
 
 Array::Array(ProgramID pid,
+             NodeID nid,
              const Source& src,
              const Type* subtype,
              const Expression* cnt,
              AttributeList attrs)
-    : Base(pid, src), type(subtype), count(cnt), attributes(attrs) {}
+    : Base(pid, nid, src), type(subtype), count(cnt), attributes(attrs) {}
 
 Array::Array(Array&&) = default;
 
diff --git a/src/tint/ast/array.h b/src/tint/ast/array.h
index e92902d..ccc31ab 100644
--- a/src/tint/ast/array.h
+++ b/src/tint/ast/array.h
@@ -32,12 +32,14 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param subtype the type of the array elements
     /// @param count the number of elements in the array. nullptr represents a
     /// runtime-sized array.
     /// @param attributes the array attributes
     Array(ProgramID pid,
+          NodeID nid,
           const Source& src,
           const Type* subtype,
           const Expression* count,
diff --git a/src/tint/ast/assignment_statement.cc b/src/tint/ast/assignment_statement.cc
index d7d7bc5..6a835b8 100644
--- a/src/tint/ast/assignment_statement.cc
+++ b/src/tint/ast/assignment_statement.cc
@@ -21,10 +21,11 @@
 namespace tint::ast {
 
 AssignmentStatement::AssignmentStatement(ProgramID pid,
+                                         NodeID nid,
                                          const Source& src,
                                          const Expression* l,
                                          const Expression* r)
-    : Base(pid, src), lhs(l), rhs(r) {
+    : Base(pid, nid, src), lhs(l), rhs(r) {
     TINT_ASSERT(AST, lhs);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, lhs, program_id);
     TINT_ASSERT(AST, rhs);
diff --git a/src/tint/ast/assignment_statement.h b/src/tint/ast/assignment_statement.h
index 9def075..6b8c412 100644
--- a/src/tint/ast/assignment_statement.h
+++ b/src/tint/ast/assignment_statement.h
@@ -24,11 +24,13 @@
 class AssignmentStatement final : public Castable<AssignmentStatement, Statement> {
   public:
     /// Constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the assignment statement source
     /// @param lhs the left side of the expression
     /// @param rhs the right side of the expression
-    AssignmentStatement(ProgramID program_id,
+    AssignmentStatement(ProgramID pid,
+                        NodeID nid,
                         const Source& source,
                         const Expression* lhs,
                         const Expression* rhs);
diff --git a/src/tint/ast/ast_type.cc b/src/tint/ast/ast_type.cc
index ec247c9..768493f 100644
--- a/src/tint/ast/ast_type.cc
+++ b/src/tint/ast/ast_type.cc
@@ -30,7 +30,7 @@
 
 namespace tint::ast {
 
-Type::Type(ProgramID pid, const Source& src) : Base(pid, src) {}
+Type::Type(ProgramID pid, NodeID nid, const Source& src) : Base(pid, nid, src) {}
 
 Type::Type(Type&&) = default;
 
diff --git a/src/tint/ast/atomic.cc b/src/tint/ast/atomic.cc
index ce7019b..9914c6a 100644
--- a/src/tint/ast/atomic.cc
+++ b/src/tint/ast/atomic.cc
@@ -20,8 +20,8 @@
 
 namespace tint::ast {
 
-Atomic::Atomic(ProgramID pid, const Source& src, const Type* const subtype)
-    : Base(pid, src), type(subtype) {}
+Atomic::Atomic(ProgramID pid, NodeID nid, const Source& src, const Type* const subtype)
+    : Base(pid, nid, src), type(subtype) {}
 
 std::string Atomic::FriendlyName(const SymbolTable& symbols) const {
     std::ostringstream out;
diff --git a/src/tint/ast/atomic.h b/src/tint/ast/atomic.h
index 5f63422..689871e 100644
--- a/src/tint/ast/atomic.h
+++ b/src/tint/ast/atomic.h
@@ -26,9 +26,10 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param subtype the pointee type
-    Atomic(ProgramID pid, const Source& src, const Type* const subtype);
+    Atomic(ProgramID pid, NodeID nid, const Source& src, const Type* const subtype);
     /// Move constructor
     Atomic(Atomic&&);
     ~Atomic() override;
diff --git a/src/tint/ast/attribute.h b/src/tint/ast/attribute.h
index cb9bf76..68c4435 100644
--- a/src/tint/ast/attribute.h
+++ b/src/tint/ast/attribute.h
@@ -33,8 +33,9 @@
   protected:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    Attribute(ProgramID pid, const Source& src) : Base(pid, src) {}
+    Attribute(ProgramID pid, NodeID nid, const Source& src) : Base(pid, nid, src) {}
 };
 
 /// A list of attributes
diff --git a/src/tint/ast/binary_expression.cc b/src/tint/ast/binary_expression.cc
index e3ccd8b..ebf704e 100644
--- a/src/tint/ast/binary_expression.cc
+++ b/src/tint/ast/binary_expression.cc
@@ -21,11 +21,12 @@
 namespace tint::ast {
 
 BinaryExpression::BinaryExpression(ProgramID pid,
+                                   NodeID nid,
                                    const Source& src,
                                    BinaryOp o,
                                    const Expression* l,
                                    const Expression* r)
-    : Base(pid, src), op(o), lhs(l), rhs(r) {
+    : Base(pid, nid, src), op(o), lhs(l), rhs(r) {
     TINT_ASSERT(AST, lhs);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, lhs, program_id);
     TINT_ASSERT(AST, rhs);
diff --git a/src/tint/ast/binary_expression.h b/src/tint/ast/binary_expression.h
index ad59da4..cdc5960 100644
--- a/src/tint/ast/binary_expression.h
+++ b/src/tint/ast/binary_expression.h
@@ -46,12 +46,14 @@
 class BinaryExpression final : public Castable<BinaryExpression, Expression> {
   public:
     /// Constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the binary expression source
     /// @param op the operation type
     /// @param lhs the left side of the expression
     /// @param rhs the right side of the expression
-    BinaryExpression(ProgramID program_id,
+    BinaryExpression(ProgramID pid,
+                     NodeID nid,
                      const Source& source,
                      BinaryOp op,
                      const Expression* lhs,
diff --git a/src/tint/ast/binding_attribute.cc b/src/tint/ast/binding_attribute.cc
index b9282f2..8180f99 100644
--- a/src/tint/ast/binding_attribute.cc
+++ b/src/tint/ast/binding_attribute.cc
@@ -22,8 +22,8 @@
 
 namespace tint::ast {
 
-BindingAttribute::BindingAttribute(ProgramID pid, const Source& src, uint32_t val)
-    : Base(pid, src), value(val) {}
+BindingAttribute::BindingAttribute(ProgramID pid, NodeID nid, const Source& src, uint32_t val)
+    : Base(pid, nid, src), value(val) {}
 
 BindingAttribute::~BindingAttribute() = default;
 
diff --git a/src/tint/ast/binding_attribute.h b/src/tint/ast/binding_attribute.h
index 33c5f69..b5379b8 100644
--- a/src/tint/ast/binding_attribute.h
+++ b/src/tint/ast/binding_attribute.h
@@ -26,9 +26,10 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param value the binding value
-    BindingAttribute(ProgramID pid, const Source& src, uint32_t value);
+    BindingAttribute(ProgramID pid, NodeID nid, const Source& src, uint32_t value);
     ~BindingAttribute() override;
 
     /// @returns the WGSL name for the attribute
diff --git a/src/tint/ast/bitcast_expression.cc b/src/tint/ast/bitcast_expression.cc
index a81c5dd..5cabf67 100644
--- a/src/tint/ast/bitcast_expression.cc
+++ b/src/tint/ast/bitcast_expression.cc
@@ -21,10 +21,11 @@
 namespace tint::ast {
 
 BitcastExpression::BitcastExpression(ProgramID pid,
+                                     NodeID nid,
                                      const Source& src,
                                      const Type* t,
                                      const Expression* e)
-    : Base(pid, src), type(t), expr(e) {
+    : Base(pid, nid, src), type(t), expr(e) {
     TINT_ASSERT(AST, type);
     TINT_ASSERT(AST, expr);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, expr, program_id);
diff --git a/src/tint/ast/bitcast_expression.h b/src/tint/ast/bitcast_expression.h
index a231cd2..66952b7 100644
--- a/src/tint/ast/bitcast_expression.h
+++ b/src/tint/ast/bitcast_expression.h
@@ -28,11 +28,13 @@
 class BitcastExpression final : public Castable<BitcastExpression, Expression> {
   public:
     /// Constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the bitcast expression source
     /// @param type the type
     /// @param expr the expr
-    BitcastExpression(ProgramID program_id,
+    BitcastExpression(ProgramID pid,
+                      NodeID nid,
                       const Source& source,
                       const Type* type,
                       const Expression* expr);
diff --git a/src/tint/ast/block_statement.cc b/src/tint/ast/block_statement.cc
index 7d4f492..4b39121 100644
--- a/src/tint/ast/block_statement.cc
+++ b/src/tint/ast/block_statement.cc
@@ -20,8 +20,11 @@
 
 namespace tint::ast {
 
-BlockStatement::BlockStatement(ProgramID pid, const Source& src, const StatementList& stmts)
-    : Base(pid, src), statements(std::move(stmts)) {
+BlockStatement::BlockStatement(ProgramID pid,
+                               NodeID nid,
+                               const Source& src,
+                               const StatementList& stmts)
+    : Base(pid, nid, src), statements(std::move(stmts)) {
     for (auto* stmt : statements) {
         TINT_ASSERT(AST, stmt);
         TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, stmt, program_id);
diff --git a/src/tint/ast/block_statement.h b/src/tint/ast/block_statement.h
index 48ea35a..c67ecd0 100644
--- a/src/tint/ast/block_statement.h
+++ b/src/tint/ast/block_statement.h
@@ -25,10 +25,14 @@
 class BlockStatement final : public Castable<BlockStatement, Statement> {
   public:
     /// Constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the block statement source
     /// @param statements the statements
-    BlockStatement(ProgramID program_id, const Source& source, const StatementList& statements);
+    BlockStatement(ProgramID pid,
+                   NodeID nid,
+                   const Source& source,
+                   const StatementList& statements);
     /// Move constructor
     BlockStatement(BlockStatement&&);
     ~BlockStatement() override;
diff --git a/src/tint/ast/bool.cc b/src/tint/ast/bool.cc
index af951e7..9b326eb 100644
--- a/src/tint/ast/bool.cc
+++ b/src/tint/ast/bool.cc
@@ -20,7 +20,7 @@
 
 namespace tint::ast {
 
-Bool::Bool(ProgramID pid, const Source& src) : Base(pid, src) {}
+Bool::Bool(ProgramID pid, NodeID nid, const Source& src) : Base(pid, nid, src) {}
 
 Bool::Bool(Bool&&) = default;
 
diff --git a/src/tint/ast/bool.h b/src/tint/ast/bool.h
index bfe3b78..d61e49d 100644
--- a/src/tint/ast/bool.h
+++ b/src/tint/ast/bool.h
@@ -32,8 +32,9 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    Bool(ProgramID pid, const Source& src);
+    Bool(ProgramID pid, NodeID nid, const Source& src);
     /// Move constructor
     Bool(Bool&&);
     ~Bool() override;
diff --git a/src/tint/ast/bool_literal_expression.cc b/src/tint/ast/bool_literal_expression.cc
index cfaacb9..10ab4f0 100644
--- a/src/tint/ast/bool_literal_expression.cc
+++ b/src/tint/ast/bool_literal_expression.cc
@@ -20,8 +20,8 @@
 
 namespace tint::ast {
 
-BoolLiteralExpression::BoolLiteralExpression(ProgramID pid, const Source& src, bool val)
-    : Base(pid, src), value(val) {}
+BoolLiteralExpression::BoolLiteralExpression(ProgramID pid, NodeID nid, const Source& src, bool val)
+    : Base(pid, nid, src), value(val) {}
 
 BoolLiteralExpression::~BoolLiteralExpression() = default;
 
diff --git a/src/tint/ast/bool_literal_expression.h b/src/tint/ast/bool_literal_expression.h
index f2c4c3f..bebd924 100644
--- a/src/tint/ast/bool_literal_expression.h
+++ b/src/tint/ast/bool_literal_expression.h
@@ -26,9 +26,10 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param value the bool literals value
-    BoolLiteralExpression(ProgramID pid, const Source& src, bool value);
+    BoolLiteralExpression(ProgramID pid, NodeID nid, const Source& src, bool value);
     ~BoolLiteralExpression() override;
 
     /// Clones this node and all transitive child nodes using the `CloneContext`
diff --git a/src/tint/ast/break_statement.cc b/src/tint/ast/break_statement.cc
index 0290014..ecd5f06 100644
--- a/src/tint/ast/break_statement.cc
+++ b/src/tint/ast/break_statement.cc
@@ -20,7 +20,8 @@
 
 namespace tint::ast {
 
-BreakStatement::BreakStatement(ProgramID pid, const Source& src) : Base(pid, src) {}
+BreakStatement::BreakStatement(ProgramID pid, NodeID nid, const Source& src)
+    : Base(pid, nid, src) {}
 
 BreakStatement::BreakStatement(BreakStatement&&) = default;
 
diff --git a/src/tint/ast/break_statement.h b/src/tint/ast/break_statement.h
index 29e5eeb..92f67b7 100644
--- a/src/tint/ast/break_statement.h
+++ b/src/tint/ast/break_statement.h
@@ -24,8 +24,9 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    BreakStatement(ProgramID pid, const Source& src);
+    BreakStatement(ProgramID pid, NodeID nid, const Source& src);
     /// Move constructor
     BreakStatement(BreakStatement&&);
     ~BreakStatement() override;
diff --git a/src/tint/ast/builtin_attribute.cc b/src/tint/ast/builtin_attribute.cc
index 03e47b6..30e7cc6 100644
--- a/src/tint/ast/builtin_attribute.cc
+++ b/src/tint/ast/builtin_attribute.cc
@@ -22,8 +22,8 @@
 
 namespace tint::ast {
 
-BuiltinAttribute::BuiltinAttribute(ProgramID pid, const Source& src, Builtin b)
-    : Base(pid, src), builtin(b) {}
+BuiltinAttribute::BuiltinAttribute(ProgramID pid, NodeID nid, const Source& src, Builtin b)
+    : Base(pid, nid, src), builtin(b) {}
 
 BuiltinAttribute::~BuiltinAttribute() = default;
 
diff --git a/src/tint/ast/builtin_attribute.h b/src/tint/ast/builtin_attribute.h
index 75898be..d0b3208 100644
--- a/src/tint/ast/builtin_attribute.h
+++ b/src/tint/ast/builtin_attribute.h
@@ -27,9 +27,10 @@
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param builtin the builtin value
-    BuiltinAttribute(ProgramID pid, const Source& src, Builtin builtin);
+    BuiltinAttribute(ProgramID pid, NodeID nid, const Source& src, Builtin builtin);
     ~BuiltinAttribute() override;
 
     /// @returns the WGSL name for the attribute
diff --git a/src/tint/ast/call_expression.cc b/src/tint/ast/call_expression.cc
index 68b6dc3..03eecf5 100644
--- a/src/tint/ast/call_expression.cc
+++ b/src/tint/ast/call_expression.cc
@@ -34,10 +34,11 @@
 }  // namespace
 
 CallExpression::CallExpression(ProgramID pid,
+                               NodeID nid,
                                const Source& src,
                                const IdentifierExpression* name,
                                ExpressionList a)
-    : Base(pid, src), target(ToTarget(name)), args(a) {
+    : Base(pid, nid, src), target(ToTarget(name)), args(a) {
     TINT_ASSERT(AST, name);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, name, program_id);
     for (auto* arg : args) {
@@ -46,8 +47,12 @@
     }
 }
 
-CallExpression::CallExpression(ProgramID pid, const Source& src, const Type* type, ExpressionList a)
-    : Base(pid, src), target(ToTarget(type)), args(a) {
+CallExpression::CallExpression(ProgramID pid,
+                               NodeID nid,
+                               const Source& src,
+                               const Type* type,
+                               ExpressionList a)
+    : Base(pid, nid, src), target(ToTarget(type)), args(a) {
     TINT_ASSERT(AST, type);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, type, program_id);
     for (auto* arg : args) {
diff --git a/src/tint/ast/call_expression.h b/src/tint/ast/call_expression.h
index 9f19711..e020429 100644
--- a/src/tint/ast/call_expression.h
+++ b/src/tint/ast/call_expression.h
@@ -33,21 +33,25 @@
 class CallExpression final : public Castable<CallExpression, Expression> {
   public:
     /// Constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the call expression source
     /// @param name the function or type name
     /// @param args the arguments
-    CallExpression(ProgramID program_id,
+    CallExpression(ProgramID pid,
+                   NodeID nid,
                    const Source& source,
                    const IdentifierExpression* name,
                    ExpressionList args);
 
     /// Constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the call expression source
     /// @param type the type
     /// @param args the arguments
-    CallExpression(ProgramID program_id,
+    CallExpression(ProgramID pid,
+                   NodeID nid,
                    const Source& source,
                    const Type* type,
                    ExpressionList args);
diff --git a/src/tint/ast/call_statement.cc b/src/tint/ast/call_statement.cc
index 5e98fc9..597e30f 100644
--- a/src/tint/ast/call_statement.cc
+++ b/src/tint/ast/call_statement.cc
@@ -20,8 +20,11 @@
 
 namespace tint::ast {
 
-CallStatement::CallStatement(ProgramID pid, const Source& src, const CallExpression* call)
-    : Base(pid, src), expr(call) {
+CallStatement::CallStatement(ProgramID pid,
+                             NodeID nid,
+                             const Source& src,
+                             const CallExpression* call)
+    : Base(pid, nid, src), expr(call) {
     TINT_ASSERT(AST, expr);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, expr, program_id);
 }
diff --git a/src/tint/ast/call_statement.h b/src/tint/ast/call_statement.h
index d0d9f53..daf0b3f 100644
--- a/src/tint/ast/call_statement.h
+++ b/src/tint/ast/call_statement.h
@@ -25,9 +25,10 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node for the statement
     /// @param call the function
-    CallStatement(ProgramID pid, const Source& src, const CallExpression* call);
+    CallStatement(ProgramID pid, NodeID nid, const Source& src, const CallExpression* call);
     /// Move constructor
     CallStatement(CallStatement&&);
     ~CallStatement() override;
diff --git a/src/tint/ast/case_statement.cc b/src/tint/ast/case_statement.cc
index bf1f0bf..55be8e3 100644
--- a/src/tint/ast/case_statement.cc
+++ b/src/tint/ast/case_statement.cc
@@ -21,10 +21,11 @@
 namespace tint::ast {
 
 CaseStatement::CaseStatement(ProgramID pid,
+                             NodeID nid,
                              const Source& src,
                              CaseSelectorList s,
                              const BlockStatement* b)
-    : Base(pid, src), selectors(s), body(b) {
+    : Base(pid, nid, src), selectors(s), body(b) {
     TINT_ASSERT(AST, body);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, body, program_id);
     for (auto* selector : selectors) {
diff --git a/src/tint/ast/case_statement.h b/src/tint/ast/case_statement.h
index 19ca693..613b721 100644
--- a/src/tint/ast/case_statement.h
+++ b/src/tint/ast/case_statement.h
@@ -30,10 +30,12 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param selectors the case selectors
     /// @param body the case body
     CaseStatement(ProgramID pid,
+                  NodeID nid,
                   const Source& src,
                   CaseSelectorList selectors,
                   const BlockStatement* body);
diff --git a/src/tint/ast/compound_assignment_statement.cc b/src/tint/ast/compound_assignment_statement.cc
index 848d500..f752862 100644
--- a/src/tint/ast/compound_assignment_statement.cc
+++ b/src/tint/ast/compound_assignment_statement.cc
@@ -21,11 +21,12 @@
 namespace tint::ast {
 
 CompoundAssignmentStatement::CompoundAssignmentStatement(ProgramID pid,
+                                                         NodeID nid,
                                                          const Source& src,
                                                          const Expression* l,
                                                          const Expression* r,
                                                          BinaryOp o)
-    : Base(pid, src), lhs(l), rhs(r), op(o) {
+    : Base(pid, nid, src), lhs(l), rhs(r), op(o) {
     TINT_ASSERT(AST, lhs);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, lhs, program_id);
     TINT_ASSERT(AST, rhs);
diff --git a/src/tint/ast/compound_assignment_statement.h b/src/tint/ast/compound_assignment_statement.h
index ba9a558..9fbd22c 100644
--- a/src/tint/ast/compound_assignment_statement.h
+++ b/src/tint/ast/compound_assignment_statement.h
@@ -25,12 +25,14 @@
 class CompoundAssignmentStatement final : public Castable<CompoundAssignmentStatement, Statement> {
   public:
     /// Constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the compound assignment statement source
     /// @param lhs the left side of the expression
     /// @param rhs the right side of the expression
     /// @param op the binary operator
-    CompoundAssignmentStatement(ProgramID program_id,
+    CompoundAssignmentStatement(ProgramID pid,
+                                NodeID nid,
                                 const Source& source,
                                 const Expression* lhs,
                                 const Expression* rhs,
diff --git a/src/tint/ast/const.cc b/src/tint/ast/const.cc
index e13cc25..4dbbb86 100644
--- a/src/tint/ast/const.cc
+++ b/src/tint/ast/const.cc
@@ -21,12 +21,13 @@
 namespace tint::ast {
 
 Const::Const(ProgramID pid,
+             NodeID nid,
              const Source& src,
              const Symbol& sym,
              const ast::Type* ty,
              const Expression* ctor,
              AttributeList attrs)
-    : Base(pid, src, sym, ty, ctor, attrs) {
+    : Base(pid, nid, src, sym, ty, ctor, attrs) {
     TINT_ASSERT(AST, ctor != nullptr);
 }
 
diff --git a/src/tint/ast/const.h b/src/tint/ast/const.h
index 32e3ddd..48cbab8 100644
--- a/src/tint/ast/const.h
+++ b/src/tint/ast/const.h
@@ -33,13 +33,15 @@
 class Const final : public Castable<Const, Variable> {
   public:
     /// Create a 'const' creation-time value variable.
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the variable source
     /// @param sym the variable symbol
     /// @param type the declared variable type
     /// @param constructor the constructor expression. Must not be nullptr.
     /// @param attributes the variable attributes
-    Const(ProgramID program_id,
+    Const(ProgramID pid,
+          NodeID nid,
           const Source& source,
           const Symbol& sym,
           const ast::Type* type,
diff --git a/src/tint/ast/continue_statement.cc b/src/tint/ast/continue_statement.cc
index 8ae4b9c..53bd6a9 100644
--- a/src/tint/ast/continue_statement.cc
+++ b/src/tint/ast/continue_statement.cc
@@ -20,7 +20,8 @@
 
 namespace tint::ast {
 
-ContinueStatement::ContinueStatement(ProgramID pid, const Source& src) : Base(pid, src) {}
+ContinueStatement::ContinueStatement(ProgramID pid, NodeID nid, const Source& src)
+    : Base(pid, nid, src) {}
 
 ContinueStatement::ContinueStatement(ContinueStatement&&) = default;
 
diff --git a/src/tint/ast/continue_statement.h b/src/tint/ast/continue_statement.h
index 17d8586..09b8254 100644
--- a/src/tint/ast/continue_statement.h
+++ b/src/tint/ast/continue_statement.h
@@ -24,8 +24,9 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    ContinueStatement(ProgramID pid, const Source& src);
+    ContinueStatement(ProgramID pid, NodeID nid, const Source& src);
     /// Move constructor
     ContinueStatement(ContinueStatement&&);
     ~ContinueStatement() override;
diff --git a/src/tint/ast/depth_multisampled_texture.cc b/src/tint/ast/depth_multisampled_texture.cc
index 66c5a86..64998b5 100644
--- a/src/tint/ast/depth_multisampled_texture.cc
+++ b/src/tint/ast/depth_multisampled_texture.cc
@@ -28,9 +28,10 @@
 }  // namespace
 
 DepthMultisampledTexture::DepthMultisampledTexture(ProgramID pid,
+                                                   NodeID nid,
                                                    const Source& src,
                                                    TextureDimension d)
-    : Base(pid, src, d) {
+    : Base(pid, nid, src, d) {
     TINT_ASSERT(AST, IsValidDepthDimension(dim));
 }
 
diff --git a/src/tint/ast/depth_multisampled_texture.h b/src/tint/ast/depth_multisampled_texture.h
index d15ac7a..2cc8d78 100644
--- a/src/tint/ast/depth_multisampled_texture.h
+++ b/src/tint/ast/depth_multisampled_texture.h
@@ -26,9 +26,10 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param dim the dimensionality of the texture
-    DepthMultisampledTexture(ProgramID pid, const Source& src, TextureDimension dim);
+    DepthMultisampledTexture(ProgramID pid, NodeID nid, const Source& src, TextureDimension dim);
     /// Move constructor
     DepthMultisampledTexture(DepthMultisampledTexture&&);
     ~DepthMultisampledTexture() override;
diff --git a/src/tint/ast/depth_texture.cc b/src/tint/ast/depth_texture.cc
index 6c0858f..4aae6f3 100644
--- a/src/tint/ast/depth_texture.cc
+++ b/src/tint/ast/depth_texture.cc
@@ -28,8 +28,8 @@
 
 }  // namespace
 
-DepthTexture::DepthTexture(ProgramID pid, const Source& src, TextureDimension d)
-    : Base(pid, src, d) {
+DepthTexture::DepthTexture(ProgramID pid, NodeID nid, const Source& src, TextureDimension d)
+    : Base(pid, nid, src, d) {
     TINT_ASSERT(AST, IsValidDepthDimension(dim));
 }
 
diff --git a/src/tint/ast/depth_texture.h b/src/tint/ast/depth_texture.h
index 42349e3..7df34a2 100644
--- a/src/tint/ast/depth_texture.h
+++ b/src/tint/ast/depth_texture.h
@@ -26,9 +26,10 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param dim the dimensionality of the texture
-    DepthTexture(ProgramID pid, const Source& src, TextureDimension dim);
+    DepthTexture(ProgramID pid, NodeID nid, const Source& src, TextureDimension dim);
     /// Move constructor
     DepthTexture(DepthTexture&&);
     ~DepthTexture() override;
diff --git a/src/tint/ast/disable_validation_attribute.cc b/src/tint/ast/disable_validation_attribute.cc
index c5a6545..1f58799 100644
--- a/src/tint/ast/disable_validation_attribute.cc
+++ b/src/tint/ast/disable_validation_attribute.cc
@@ -20,8 +20,10 @@
 
 namespace tint::ast {
 
-DisableValidationAttribute::DisableValidationAttribute(ProgramID pid, DisabledValidation val)
-    : Base(pid), validation(val) {}
+DisableValidationAttribute::DisableValidationAttribute(ProgramID pid,
+                                                       NodeID nid,
+                                                       DisabledValidation val)
+    : Base(pid, nid), validation(val) {}
 
 DisableValidationAttribute::~DisableValidationAttribute() = default;
 
@@ -46,7 +48,8 @@
 }
 
 const DisableValidationAttribute* DisableValidationAttribute::Clone(CloneContext* ctx) const {
-    return ctx->dst->ASTNodes().Create<DisableValidationAttribute>(ctx->dst->ID(), validation);
+    return ctx->dst->ASTNodes().Create<DisableValidationAttribute>(
+        ctx->dst->ID(), ctx->dst->AllocateNodeID(), validation);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/ast/disable_validation_attribute.h b/src/tint/ast/disable_validation_attribute.h
index e44f7b8..a109d18 100644
--- a/src/tint/ast/disable_validation_attribute.h
+++ b/src/tint/ast/disable_validation_attribute.h
@@ -51,9 +51,10 @@
     : public Castable<DisableValidationAttribute, InternalAttribute> {
   public:
     /// Constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param validation the validation to disable
-    explicit DisableValidationAttribute(ProgramID program_id, DisabledValidation validation);
+    explicit DisableValidationAttribute(ProgramID pid, NodeID nid, DisabledValidation validation);
 
     /// Destructor
     ~DisableValidationAttribute() override;
diff --git a/src/tint/ast/discard_statement.cc b/src/tint/ast/discard_statement.cc
index 7ca673f..fc9e75b 100644
--- a/src/tint/ast/discard_statement.cc
+++ b/src/tint/ast/discard_statement.cc
@@ -20,7 +20,8 @@
 
 namespace tint::ast {
 
-DiscardStatement::DiscardStatement(ProgramID pid, const Source& src) : Base(pid, src) {}
+DiscardStatement::DiscardStatement(ProgramID pid, NodeID nid, const Source& src)
+    : Base(pid, nid, src) {}
 
 DiscardStatement::DiscardStatement(DiscardStatement&&) = default;
 
diff --git a/src/tint/ast/discard_statement.h b/src/tint/ast/discard_statement.h
index 9d18c74..272cc2d 100644
--- a/src/tint/ast/discard_statement.h
+++ b/src/tint/ast/discard_statement.h
@@ -24,8 +24,9 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    DiscardStatement(ProgramID pid, const Source& src);
+    DiscardStatement(ProgramID pid, NodeID nid, const Source& src);
     /// Move constructor
     DiscardStatement(DiscardStatement&&);
     ~DiscardStatement() override;
diff --git a/src/tint/ast/enable.cc b/src/tint/ast/enable.cc
index ef43200..9da44c7 100644
--- a/src/tint/ast/enable.cc
+++ b/src/tint/ast/enable.cc
@@ -21,7 +21,8 @@
 
 namespace tint::ast {
 
-Enable::Enable(ProgramID pid, const Source& src, Extension ext) : Base(pid, src), extension(ext) {}
+Enable::Enable(ProgramID pid, NodeID nid, const Source& src, Extension ext)
+    : Base(pid, nid, src), extension(ext) {}
 
 Enable::Enable(Enable&&) = default;
 
diff --git a/src/tint/ast/enable.h b/src/tint/ast/enable.h
index 674d9cb..df8c5e9 100644
--- a/src/tint/ast/enable.h
+++ b/src/tint/ast/enable.h
@@ -33,9 +33,10 @@
   public:
     /// Create a extension
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param ext the extension
-    Enable(ProgramID pid, const Source& src, Extension ext);
+    Enable(ProgramID pid, NodeID nid, const Source& src, Extension ext);
     /// Move constructor
     Enable(Enable&&);
 
diff --git a/src/tint/ast/expression.cc b/src/tint/ast/expression.cc
index a7f23aa..17b3dc2 100644
--- a/src/tint/ast/expression.cc
+++ b/src/tint/ast/expression.cc
@@ -21,7 +21,7 @@
 
 namespace tint::ast {
 
-Expression::Expression(ProgramID pid, const Source& src) : Base(pid, src) {}
+Expression::Expression(ProgramID pid, NodeID nid, const Source& src) : Base(pid, nid, src) {}
 
 Expression::Expression(Expression&&) = default;
 
diff --git a/src/tint/ast/expression.h b/src/tint/ast/expression.h
index dc69ff8..2690b93 100644
--- a/src/tint/ast/expression.h
+++ b/src/tint/ast/expression.h
@@ -31,8 +31,9 @@
   protected:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    Expression(ProgramID pid, const Source& src);
+    Expression(ProgramID pid, NodeID nid, const Source& src);
     /// Move constructor
     Expression(Expression&&);
 };
diff --git a/src/tint/ast/external_texture.cc b/src/tint/ast/external_texture.cc
index b88de90..4881913 100644
--- a/src/tint/ast/external_texture.cc
+++ b/src/tint/ast/external_texture.cc
@@ -21,8 +21,8 @@
 namespace tint::ast {
 
 // ExternalTexture::ExternalTexture() : Base(ast::TextureDimension::k2d) {}
-ExternalTexture::ExternalTexture(ProgramID pid, const Source& src)
-    : Base(pid, src, ast::TextureDimension::k2d) {}
+ExternalTexture::ExternalTexture(ProgramID pid, NodeID nid, const Source& src)
+    : Base(pid, nid, src, ast::TextureDimension::k2d) {}
 
 ExternalTexture::ExternalTexture(ExternalTexture&&) = default;
 
diff --git a/src/tint/ast/external_texture.h b/src/tint/ast/external_texture.h
index 17224cf..f2d68b4 100644
--- a/src/tint/ast/external_texture.h
+++ b/src/tint/ast/external_texture.h
@@ -26,8 +26,9 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    ExternalTexture(ProgramID pid, const Source& src);
+    ExternalTexture(ProgramID pid, NodeID nid, const Source& src);
 
     /// Move constructor
     ExternalTexture(ExternalTexture&&);
diff --git a/src/tint/ast/f16.cc b/src/tint/ast/f16.cc
index 0eb1be5..dd3e48e 100644
--- a/src/tint/ast/f16.cc
+++ b/src/tint/ast/f16.cc
@@ -20,7 +20,7 @@
 
 namespace tint::ast {
 
-F16::F16(ProgramID pid, const Source& src) : Base(pid, src) {}
+F16::F16(ProgramID pid, NodeID nid, const Source& src) : Base(pid, nid, src) {}
 
 F16::F16(F16&&) = default;
 
diff --git a/src/tint/ast/f16.h b/src/tint/ast/f16.h
index 1b84f09..c444a20 100644
--- a/src/tint/ast/f16.h
+++ b/src/tint/ast/f16.h
@@ -26,8 +26,9 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    F16(ProgramID pid, const Source& src);
+    F16(ProgramID pid, NodeID nid, const Source& src);
     /// Move constructor
     F16(F16&&);
     ~F16() override;
diff --git a/src/tint/ast/f32.cc b/src/tint/ast/f32.cc
index b731e65..0ae354a 100644
--- a/src/tint/ast/f32.cc
+++ b/src/tint/ast/f32.cc
@@ -20,7 +20,7 @@
 
 namespace tint::ast {
 
-F32::F32(ProgramID pid, const Source& src) : Base(pid, src) {}
+F32::F32(ProgramID pid, NodeID nid, const Source& src) : Base(pid, nid, src) {}
 
 F32::F32(F32&&) = default;
 
diff --git a/src/tint/ast/f32.h b/src/tint/ast/f32.h
index db81491..5176c45 100644
--- a/src/tint/ast/f32.h
+++ b/src/tint/ast/f32.h
@@ -26,8 +26,9 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    F32(ProgramID pid, const Source& src);
+    F32(ProgramID pid, NodeID nid, const Source& src);
     /// Move constructor
     F32(F32&&);
     ~F32() override;
diff --git a/src/tint/ast/fallthrough_statement.cc b/src/tint/ast/fallthrough_statement.cc
index 446534d..0d30ae3 100644
--- a/src/tint/ast/fallthrough_statement.cc
+++ b/src/tint/ast/fallthrough_statement.cc
@@ -20,7 +20,8 @@
 
 namespace tint::ast {
 
-FallthroughStatement::FallthroughStatement(ProgramID pid, const Source& src) : Base(pid, src) {}
+FallthroughStatement::FallthroughStatement(ProgramID pid, NodeID nid, const Source& src)
+    : Base(pid, nid, src) {}
 
 FallthroughStatement::FallthroughStatement(FallthroughStatement&&) = default;
 
diff --git a/src/tint/ast/fallthrough_statement.h b/src/tint/ast/fallthrough_statement.h
index b313efb..da2fd3d 100644
--- a/src/tint/ast/fallthrough_statement.h
+++ b/src/tint/ast/fallthrough_statement.h
@@ -24,8 +24,9 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    FallthroughStatement(ProgramID pid, const Source& src);
+    FallthroughStatement(ProgramID pid, NodeID nid, const Source& src);
     /// Move constructor
     FallthroughStatement(FallthroughStatement&&);
     ~FallthroughStatement() override;
diff --git a/src/tint/ast/float_literal_expression.cc b/src/tint/ast/float_literal_expression.cc
index 36cb42a..524b56e 100644
--- a/src/tint/ast/float_literal_expression.cc
+++ b/src/tint/ast/float_literal_expression.cc
@@ -23,10 +23,11 @@
 namespace tint::ast {
 
 FloatLiteralExpression::FloatLiteralExpression(ProgramID pid,
+                                               NodeID nid,
                                                const Source& src,
                                                double val,
                                                Suffix suf)
-    : Base(pid, src), value(val), suffix(suf) {}
+    : Base(pid, nid, src), value(val), suffix(suf) {}
 
 FloatLiteralExpression::~FloatLiteralExpression() = default;
 
diff --git a/src/tint/ast/float_literal_expression.h b/src/tint/ast/float_literal_expression.h
index 7f03caf..7f3cd12 100644
--- a/src/tint/ast/float_literal_expression.h
+++ b/src/tint/ast/float_literal_expression.h
@@ -36,10 +36,11 @@
 
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param val the literal value
     /// @param suf the literal suffix
-    FloatLiteralExpression(ProgramID pid, const Source& src, double val, Suffix suf);
+    FloatLiteralExpression(ProgramID pid, NodeID nid, const Source& src, double val, Suffix suf);
     ~FloatLiteralExpression() override;
 
     /// Clones this node and all transitive child nodes using the `CloneContext`
diff --git a/src/tint/ast/for_loop_statement.cc b/src/tint/ast/for_loop_statement.cc
index 804389c..aba956d 100644
--- a/src/tint/ast/for_loop_statement.cc
+++ b/src/tint/ast/for_loop_statement.cc
@@ -21,12 +21,13 @@
 namespace tint::ast {
 
 ForLoopStatement::ForLoopStatement(ProgramID pid,
+                                   NodeID nid,
                                    const Source& src,
                                    const Statement* init,
                                    const Expression* cond,
                                    const Statement* cont,
                                    const BlockStatement* b)
-    : Base(pid, src), initializer(init), condition(cond), continuing(cont), body(b) {
+    : Base(pid, nid, src), initializer(init), condition(cond), continuing(cont), body(b) {
     TINT_ASSERT(AST, body);
 
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, initializer, program_id);
diff --git a/src/tint/ast/for_loop_statement.h b/src/tint/ast/for_loop_statement.h
index 464ea49..59e0587 100644
--- a/src/tint/ast/for_loop_statement.h
+++ b/src/tint/ast/for_loop_statement.h
@@ -25,14 +25,16 @@
 class ForLoopStatement final : public Castable<ForLoopStatement, Statement> {
   public:
     /// Constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the for loop statement source
     /// @param initializer the optional loop initializer statement
     /// @param condition the optional loop condition expression
     /// @param continuing the optional continuing statement
     /// @param body the loop body
-    ForLoopStatement(ProgramID program_id,
-                     Source const& source,
+    ForLoopStatement(ProgramID pid,
+                     NodeID nid,
+                     const Source& source,
                      const Statement* initializer,
                      const Expression* condition,
                      const Statement* continuing,
diff --git a/src/tint/ast/function.cc b/src/tint/ast/function.cc
index 84d80d7..49bfb9b 100644
--- a/src/tint/ast/function.cc
+++ b/src/tint/ast/function.cc
@@ -23,6 +23,7 @@
 namespace tint::ast {
 
 Function::Function(ProgramID pid,
+                   NodeID nid,
                    const Source& src,
                    Symbol sym,
                    ParameterList parameters,
@@ -30,7 +31,7 @@
                    const BlockStatement* b,
                    AttributeList attrs,
                    AttributeList return_type_attrs)
-    : Base(pid, src),
+    : Base(pid, nid, src),
       symbol(sym),
       params(std::move(parameters)),
       return_type(return_ty),
diff --git a/src/tint/ast/function.h b/src/tint/ast/function.h
index d849486..ed7f5b2 100644
--- a/src/tint/ast/function.h
+++ b/src/tint/ast/function.h
@@ -35,7 +35,8 @@
 class Function final : public Castable<Function, Node> {
   public:
     /// Create a function
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the variable source
     /// @param symbol the function symbol
     /// @param params the function parameters
@@ -43,7 +44,8 @@
     /// @param body the function body
     /// @param attributes the function attributes
     /// @param return_type_attributes the return type attributes
-    Function(ProgramID program_id,
+    Function(ProgramID pid,
+             NodeID nid,
              const Source& source,
              Symbol symbol,
              ParameterList params,
diff --git a/src/tint/ast/group_attribute.cc b/src/tint/ast/group_attribute.cc
index 394a690..9d01111 100644
--- a/src/tint/ast/group_attribute.cc
+++ b/src/tint/ast/group_attribute.cc
@@ -22,8 +22,8 @@
 
 namespace tint::ast {
 
-GroupAttribute::GroupAttribute(ProgramID pid, const Source& src, uint32_t val)
-    : Base(pid, src), value(val) {}
+GroupAttribute::GroupAttribute(ProgramID pid, NodeID nid, const Source& src, uint32_t val)
+    : Base(pid, nid, src), value(val) {}
 
 GroupAttribute::~GroupAttribute() = default;
 
diff --git a/src/tint/ast/group_attribute.h b/src/tint/ast/group_attribute.h
index a559461..66d6ccb 100644
--- a/src/tint/ast/group_attribute.h
+++ b/src/tint/ast/group_attribute.h
@@ -26,9 +26,10 @@
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param value the group value
-    GroupAttribute(ProgramID pid, const Source& src, uint32_t value);
+    GroupAttribute(ProgramID pid, NodeID nid, const Source& src, uint32_t value);
     ~GroupAttribute() override;
 
     /// @returns the WGSL name for the attribute
diff --git a/src/tint/ast/i32.cc b/src/tint/ast/i32.cc
index 46fe75e..ffdf30d 100644
--- a/src/tint/ast/i32.cc
+++ b/src/tint/ast/i32.cc
@@ -20,7 +20,7 @@
 
 namespace tint::ast {
 
-I32::I32(ProgramID pid, const Source& src) : Base(pid, src) {}
+I32::I32(ProgramID pid, NodeID nid, const Source& src) : Base(pid, nid, src) {}
 
 I32::I32(I32&&) = default;
 
diff --git a/src/tint/ast/i32.h b/src/tint/ast/i32.h
index acafd37..d2c951c 100644
--- a/src/tint/ast/i32.h
+++ b/src/tint/ast/i32.h
@@ -26,8 +26,9 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    I32(ProgramID pid, const Source& src);
+    I32(ProgramID pid, NodeID nid, const Source& src);
     /// Move constructor
     I32(I32&&);
     ~I32() override;
diff --git a/src/tint/ast/id_attribute.cc b/src/tint/ast/id_attribute.cc
index b6e1957..75d62c6 100644
--- a/src/tint/ast/id_attribute.cc
+++ b/src/tint/ast/id_attribute.cc
@@ -22,8 +22,8 @@
 
 namespace tint::ast {
 
-IdAttribute::IdAttribute(ProgramID pid, const Source& src, uint32_t val)
-    : Base(pid, src), value(val) {}
+IdAttribute::IdAttribute(ProgramID pid, NodeID nid, const Source& src, uint32_t val)
+    : Base(pid, nid, src), value(val) {}
 
 IdAttribute::~IdAttribute() = default;
 
diff --git a/src/tint/ast/id_attribute.h b/src/tint/ast/id_attribute.h
index 5e3ec12..ca2a358 100644
--- a/src/tint/ast/id_attribute.h
+++ b/src/tint/ast/id_attribute.h
@@ -26,9 +26,10 @@
   public:
     /// Create an id attribute.
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param val the numeric id value
-    IdAttribute(ProgramID pid, const Source& src, uint32_t val);
+    IdAttribute(ProgramID pid, NodeID nid, const Source& src, uint32_t val);
     ~IdAttribute() override;
 
     /// @returns the WGSL name for the attribute
diff --git a/src/tint/ast/identifier_expression.cc b/src/tint/ast/identifier_expression.cc
index 453ae69..34eadeb 100644
--- a/src/tint/ast/identifier_expression.cc
+++ b/src/tint/ast/identifier_expression.cc
@@ -20,8 +20,8 @@
 
 namespace tint::ast {
 
-IdentifierExpression::IdentifierExpression(ProgramID pid, const Source& src, Symbol sym)
-    : Base(pid, src), symbol(sym) {
+IdentifierExpression::IdentifierExpression(ProgramID pid, NodeID nid, const Source& src, Symbol sym)
+    : Base(pid, nid, src), symbol(sym) {
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, symbol, program_id);
     TINT_ASSERT(AST, symbol.IsValid());
 }
diff --git a/src/tint/ast/identifier_expression.h b/src/tint/ast/identifier_expression.h
index c3e1c30..b583807 100644
--- a/src/tint/ast/identifier_expression.h
+++ b/src/tint/ast/identifier_expression.h
@@ -24,9 +24,10 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param sym the symbol for the identifier
-    IdentifierExpression(ProgramID pid, const Source& src, Symbol sym);
+    IdentifierExpression(ProgramID pid, NodeID nid, const Source& src, Symbol sym);
     /// Move constructor
     IdentifierExpression(IdentifierExpression&&);
     ~IdentifierExpression() override;
diff --git a/src/tint/ast/if_statement.cc b/src/tint/ast/if_statement.cc
index c8fd374..5f7f1a7 100644
--- a/src/tint/ast/if_statement.cc
+++ b/src/tint/ast/if_statement.cc
@@ -21,11 +21,12 @@
 namespace tint::ast {
 
 IfStatement::IfStatement(ProgramID pid,
+                         NodeID nid,
                          const Source& src,
                          const Expression* cond,
                          const BlockStatement* b,
                          const Statement* else_stmt)
-    : Base(pid, src), condition(cond), body(b), else_statement(else_stmt) {
+    : Base(pid, nid, src), condition(cond), body(b), else_statement(else_stmt) {
     TINT_ASSERT(AST, condition);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, condition, program_id);
     TINT_ASSERT(AST, body);
diff --git a/src/tint/ast/if_statement.h b/src/tint/ast/if_statement.h
index 75e6eee..81d9756 100644
--- a/src/tint/ast/if_statement.h
+++ b/src/tint/ast/if_statement.h
@@ -27,11 +27,13 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param condition the if condition
     /// @param body the if body
     /// @param else_stmt the else statement, or nullptr
     IfStatement(ProgramID pid,
+                NodeID nid,
                 const Source& src,
                 const Expression* condition,
                 const BlockStatement* body,
diff --git a/src/tint/ast/increment_decrement_statement.cc b/src/tint/ast/increment_decrement_statement.cc
index 99c65cb..5b10e5f 100644
--- a/src/tint/ast/increment_decrement_statement.cc
+++ b/src/tint/ast/increment_decrement_statement.cc
@@ -21,10 +21,11 @@
 namespace tint::ast {
 
 IncrementDecrementStatement::IncrementDecrementStatement(ProgramID pid,
+                                                         NodeID nid,
                                                          const Source& src,
                                                          const Expression* l,
                                                          bool inc)
-    : Base(pid, src), lhs(l), increment(inc) {
+    : Base(pid, nid, src), lhs(l), increment(inc) {
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, lhs, program_id);
 }
 
diff --git a/src/tint/ast/increment_decrement_statement.h b/src/tint/ast/increment_decrement_statement.h
index 05b8478..ec9923a 100644
--- a/src/tint/ast/increment_decrement_statement.h
+++ b/src/tint/ast/increment_decrement_statement.h
@@ -25,10 +25,15 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param lhs the LHS expression
     /// @param inc `true` for increment, `false` for decrement
-    IncrementDecrementStatement(ProgramID pid, const Source& src, const Expression* lhs, bool inc);
+    IncrementDecrementStatement(ProgramID pid,
+                                NodeID nid,
+                                const Source& src,
+                                const Expression* lhs,
+                                bool inc);
     /// Move constructor
     IncrementDecrementStatement(IncrementDecrementStatement&&);
     ~IncrementDecrementStatement() override;
diff --git a/src/tint/ast/index_accessor_expression.cc b/src/tint/ast/index_accessor_expression.cc
index 232bc79..47f5156 100644
--- a/src/tint/ast/index_accessor_expression.cc
+++ b/src/tint/ast/index_accessor_expression.cc
@@ -21,10 +21,11 @@
 namespace tint::ast {
 
 IndexAccessorExpression::IndexAccessorExpression(ProgramID pid,
+                                                 NodeID nid,
                                                  const Source& src,
                                                  const Expression* obj,
                                                  const Expression* idx)
-    : Base(pid, src), object(obj), index(idx) {
+    : Base(pid, nid, src), object(obj), index(idx) {
     TINT_ASSERT(AST, object);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, object, program_id);
     TINT_ASSERT(AST, idx);
diff --git a/src/tint/ast/index_accessor_expression.h b/src/tint/ast/index_accessor_expression.h
index c36f6b8..0307f9e 100644
--- a/src/tint/ast/index_accessor_expression.h
+++ b/src/tint/ast/index_accessor_expression.h
@@ -23,11 +23,13 @@
 class IndexAccessorExpression final : public Castable<IndexAccessorExpression, Expression> {
   public:
     /// Constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the index accessor source
     /// @param obj the object
     /// @param idx the index expression
-    IndexAccessorExpression(ProgramID program_id,
+    IndexAccessorExpression(ProgramID pid,
+                            NodeID nid,
                             const Source& source,
                             const Expression* obj,
                             const Expression* idx);
diff --git a/src/tint/ast/int_literal_expression.cc b/src/tint/ast/int_literal_expression.cc
index 7e11f7e..502ea9d 100644
--- a/src/tint/ast/int_literal_expression.cc
+++ b/src/tint/ast/int_literal_expression.cc
@@ -21,10 +21,11 @@
 namespace tint::ast {
 
 IntLiteralExpression::IntLiteralExpression(ProgramID pid,
+                                           NodeID nid,
                                            const Source& src,
                                            int64_t val,
                                            Suffix suf)
-    : Base(pid, src), value(val), suffix(suf) {}
+    : Base(pid, nid, src), value(val), suffix(suf) {}
 
 IntLiteralExpression::~IntLiteralExpression() = default;
 
diff --git a/src/tint/ast/int_literal_expression.h b/src/tint/ast/int_literal_expression.h
index 8ff58ea..b4e184a 100644
--- a/src/tint/ast/int_literal_expression.h
+++ b/src/tint/ast/int_literal_expression.h
@@ -34,10 +34,11 @@
 
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param val the literal value
     /// @param suf the literal suffix
-    IntLiteralExpression(ProgramID pid, const Source& src, int64_t val, Suffix suf);
+    IntLiteralExpression(ProgramID pid, NodeID nid, const Source& src, int64_t val, Suffix suf);
 
     ~IntLiteralExpression() override;
 
diff --git a/src/tint/ast/internal_attribute.cc b/src/tint/ast/internal_attribute.cc
index 180e909..1b4ca9e 100644
--- a/src/tint/ast/internal_attribute.cc
+++ b/src/tint/ast/internal_attribute.cc
@@ -18,7 +18,7 @@
 
 namespace tint::ast {
 
-InternalAttribute::InternalAttribute(ProgramID pid) : Base(pid, Source{}) {}
+InternalAttribute::InternalAttribute(ProgramID pid, NodeID nid) : Base(pid, nid, Source{}) {}
 
 InternalAttribute::~InternalAttribute() = default;
 
diff --git a/src/tint/ast/internal_attribute.h b/src/tint/ast/internal_attribute.h
index bb13559..9904af8 100644
--- a/src/tint/ast/internal_attribute.h
+++ b/src/tint/ast/internal_attribute.h
@@ -28,7 +28,8 @@
   public:
     /// Constructor
     /// @param program_id the identifier of the program that owns this node
-    explicit InternalAttribute(ProgramID program_id);
+    /// @param nid the unique node identifier
+    explicit InternalAttribute(ProgramID program_id, NodeID nid);
 
     /// Destructor
     ~InternalAttribute() override;
diff --git a/src/tint/ast/interpolate_attribute.cc b/src/tint/ast/interpolate_attribute.cc
index 909e827..29e3bfe 100644
--- a/src/tint/ast/interpolate_attribute.cc
+++ b/src/tint/ast/interpolate_attribute.cc
@@ -23,10 +23,11 @@
 namespace tint::ast {
 
 InterpolateAttribute::InterpolateAttribute(ProgramID pid,
+                                           NodeID nid,
                                            const Source& src,
                                            InterpolationType ty,
                                            InterpolationSampling smpl)
-    : Base(pid, src), type(ty), sampling(smpl) {}
+    : Base(pid, nid, src), type(ty), sampling(smpl) {}
 
 InterpolateAttribute::~InterpolateAttribute() = default;
 
diff --git a/src/tint/ast/interpolate_attribute.h b/src/tint/ast/interpolate_attribute.h
index 4b2a2df..4f9ea9d 100644
--- a/src/tint/ast/interpolate_attribute.h
+++ b/src/tint/ast/interpolate_attribute.h
@@ -33,10 +33,12 @@
   public:
     /// Create an interpolate attribute.
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param type the interpolation type
     /// @param sampling the interpolation sampling
     InterpolateAttribute(ProgramID pid,
+                         NodeID nid,
                          const Source& src,
                          InterpolationType type,
                          InterpolationSampling sampling);
diff --git a/src/tint/ast/invariant_attribute.cc b/src/tint/ast/invariant_attribute.cc
index 1b0f126..1fa4e5d 100644
--- a/src/tint/ast/invariant_attribute.cc
+++ b/src/tint/ast/invariant_attribute.cc
@@ -20,7 +20,8 @@
 
 namespace tint::ast {
 
-InvariantAttribute::InvariantAttribute(ProgramID pid, const Source& src) : Base(pid, src) {}
+InvariantAttribute::InvariantAttribute(ProgramID pid, NodeID nid, const Source& src)
+    : Base(pid, nid, src) {}
 
 InvariantAttribute::~InvariantAttribute() = default;
 
diff --git a/src/tint/ast/invariant_attribute.h b/src/tint/ast/invariant_attribute.h
index 6bb42fc..9abb6a4 100644
--- a/src/tint/ast/invariant_attribute.h
+++ b/src/tint/ast/invariant_attribute.h
@@ -26,8 +26,9 @@
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    InvariantAttribute(ProgramID pid, const Source& src);
+    InvariantAttribute(ProgramID pid, NodeID nid, const Source& src);
     ~InvariantAttribute() override;
 
     /// @returns the WGSL name for the attribute
diff --git a/src/tint/ast/let.cc b/src/tint/ast/let.cc
index 8db8ec1..b42e6e6 100644
--- a/src/tint/ast/let.cc
+++ b/src/tint/ast/let.cc
@@ -21,12 +21,13 @@
 namespace tint::ast {
 
 Let::Let(ProgramID pid,
+         NodeID nid,
          const Source& src,
          const Symbol& sym,
          const ast::Type* ty,
          const Expression* ctor,
          AttributeList attrs)
-    : Base(pid, src, sym, ty, ctor, attrs) {
+    : Base(pid, nid, src, sym, ty, ctor, attrs) {
     TINT_ASSERT(AST, ctor != nullptr);
 }
 
diff --git a/src/tint/ast/let.h b/src/tint/ast/let.h
index 2c1ad7c..0f71da2 100644
--- a/src/tint/ast/let.h
+++ b/src/tint/ast/let.h
@@ -30,13 +30,15 @@
 class Let final : public Castable<Let, Variable> {
   public:
     /// Create a 'let' variable
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the variable source
     /// @param sym the variable symbol
     /// @param type the declared variable type
     /// @param constructor the constructor expression
     /// @param attributes the variable attributes
-    Let(ProgramID program_id,
+    Let(ProgramID pid,
+        NodeID nid,
         const Source& source,
         const Symbol& sym,
         const ast::Type* type,
diff --git a/src/tint/ast/literal_expression.cc b/src/tint/ast/literal_expression.cc
index d05279d..bcf62ad 100644
--- a/src/tint/ast/literal_expression.cc
+++ b/src/tint/ast/literal_expression.cc
@@ -18,7 +18,8 @@
 
 namespace tint::ast {
 
-LiteralExpression::LiteralExpression(ProgramID pid, const Source& src) : Base(pid, src) {}
+LiteralExpression::LiteralExpression(ProgramID pid, NodeID nid, const Source& src)
+    : Base(pid, nid, src) {}
 
 LiteralExpression::~LiteralExpression() = default;
 
diff --git a/src/tint/ast/literal_expression.h b/src/tint/ast/literal_expression.h
index 56fc1f0..b4b2b09 100644
--- a/src/tint/ast/literal_expression.h
+++ b/src/tint/ast/literal_expression.h
@@ -29,8 +29,9 @@
   protected:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the input source
-    LiteralExpression(ProgramID pid, const Source& src);
+    LiteralExpression(ProgramID pid, NodeID nid, const Source& src);
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/ast/location_attribute.cc b/src/tint/ast/location_attribute.cc
index 1eae823..2ea2d5d 100644
--- a/src/tint/ast/location_attribute.cc
+++ b/src/tint/ast/location_attribute.cc
@@ -22,8 +22,8 @@
 
 namespace tint::ast {
 
-LocationAttribute::LocationAttribute(ProgramID pid, const Source& src, uint32_t val)
-    : Base(pid, src), value(val) {}
+LocationAttribute::LocationAttribute(ProgramID pid, NodeID nid, const Source& src, uint32_t val)
+    : Base(pid, nid, src), value(val) {}
 
 LocationAttribute::~LocationAttribute() = default;
 
diff --git a/src/tint/ast/location_attribute.h b/src/tint/ast/location_attribute.h
index 3646c54..97c6fea 100644
--- a/src/tint/ast/location_attribute.h
+++ b/src/tint/ast/location_attribute.h
@@ -26,9 +26,10 @@
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param value the location value
-    LocationAttribute(ProgramID pid, const Source& src, uint32_t value);
+    LocationAttribute(ProgramID pid, NodeID nid, const Source& src, uint32_t value);
     ~LocationAttribute() override;
 
     /// @returns the WGSL name for the attribute
diff --git a/src/tint/ast/loop_statement.cc b/src/tint/ast/loop_statement.cc
index 9d14960..b7e7a1b 100644
--- a/src/tint/ast/loop_statement.cc
+++ b/src/tint/ast/loop_statement.cc
@@ -21,10 +21,11 @@
 namespace tint::ast {
 
 LoopStatement::LoopStatement(ProgramID pid,
+                             NodeID nid,
                              const Source& src,
                              const BlockStatement* b,
                              const BlockStatement* cont)
-    : Base(pid, src), body(b), continuing(cont) {
+    : Base(pid, nid, src), body(b), continuing(cont) {
     TINT_ASSERT(AST, body);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, body, program_id);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, continuing, program_id);
diff --git a/src/tint/ast/loop_statement.h b/src/tint/ast/loop_statement.h
index 5a044fe..d4b24cf 100644
--- a/src/tint/ast/loop_statement.h
+++ b/src/tint/ast/loop_statement.h
@@ -23,11 +23,13 @@
 class LoopStatement final : public Castable<LoopStatement, Statement> {
   public:
     /// Constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the loop statement source
     /// @param body the body statements
     /// @param continuing the continuing statements
-    LoopStatement(ProgramID program_id,
+    LoopStatement(ProgramID pid,
+                  NodeID nid,
                   const Source& source,
                   const BlockStatement* body,
                   const BlockStatement* continuing);
diff --git a/src/tint/ast/matrix.cc b/src/tint/ast/matrix.cc
index 1f74a26..6937127 100644
--- a/src/tint/ast/matrix.cc
+++ b/src/tint/ast/matrix.cc
@@ -20,8 +20,13 @@
 
 namespace tint::ast {
 
-Matrix::Matrix(ProgramID pid, const Source& src, const Type* subtype, uint32_t r, uint32_t c)
-    : Base(pid, src), type(subtype), rows(r), columns(c) {
+Matrix::Matrix(ProgramID pid,
+               NodeID nid,
+               const Source& src,
+               const Type* subtype,
+               uint32_t r,
+               uint32_t c)
+    : Base(pid, nid, src), type(subtype), rows(r), columns(c) {
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, subtype, program_id);
     TINT_ASSERT(AST, rows > 1);
     TINT_ASSERT(AST, rows < 5);
diff --git a/src/tint/ast/matrix.h b/src/tint/ast/matrix.h
index 620f28a..e778738 100644
--- a/src/tint/ast/matrix.h
+++ b/src/tint/ast/matrix.h
@@ -26,13 +26,19 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param subtype the declared type of the matrix components. May be null for
     ///        matrix constructors, where the element type will be inferred from
     ///        the constructor arguments
     /// @param rows the number of rows in the matrix
     /// @param columns the number of columns in the matrix
-    Matrix(ProgramID pid, const Source& src, const Type* subtype, uint32_t rows, uint32_t columns);
+    Matrix(ProgramID pid,
+           NodeID nid,
+           const Source& src,
+           const Type* subtype,
+           uint32_t rows,
+           uint32_t columns);
     /// Move constructor
     Matrix(Matrix&&);
     ~Matrix() override;
diff --git a/src/tint/ast/member_accessor_expression.cc b/src/tint/ast/member_accessor_expression.cc
index a087ea4..b895e3b 100644
--- a/src/tint/ast/member_accessor_expression.cc
+++ b/src/tint/ast/member_accessor_expression.cc
@@ -21,10 +21,11 @@
 namespace tint::ast {
 
 MemberAccessorExpression::MemberAccessorExpression(ProgramID pid,
+                                                   NodeID nid,
                                                    const Source& src,
                                                    const Expression* str,
                                                    const IdentifierExpression* mem)
-    : Base(pid, src), structure(str), member(mem) {
+    : Base(pid, nid, src), structure(str), member(mem) {
     TINT_ASSERT(AST, structure);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, structure, program_id);
     TINT_ASSERT(AST, member);
diff --git a/src/tint/ast/member_accessor_expression.h b/src/tint/ast/member_accessor_expression.h
index 07054ca..33284fe 100644
--- a/src/tint/ast/member_accessor_expression.h
+++ b/src/tint/ast/member_accessor_expression.h
@@ -23,11 +23,13 @@
 class MemberAccessorExpression final : public Castable<MemberAccessorExpression, Expression> {
   public:
     /// Constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the member accessor expression source
     /// @param structure the structure
     /// @param member the member
-    MemberAccessorExpression(ProgramID program_id,
+    MemberAccessorExpression(ProgramID pid,
+                             NodeID nid,
                              const Source& source,
                              const Expression* structure,
                              const IdentifierExpression* member);
diff --git a/src/tint/ast/module.cc b/src/tint/ast/module.cc
index 40dff98..7cf9282 100644
--- a/src/tint/ast/module.cc
+++ b/src/tint/ast/module.cc
@@ -23,10 +23,13 @@
 
 namespace tint::ast {
 
-Module::Module(ProgramID pid, const Source& src) : Base(pid, src) {}
+Module::Module(ProgramID pid, NodeID nid, const Source& src) : Base(pid, nid, src) {}
 
-Module::Module(ProgramID pid, const Source& src, std::vector<const ast::Node*> global_decls)
-    : Base(pid, src), global_declarations_(std::move(global_decls)) {
+Module::Module(ProgramID pid,
+               NodeID nid,
+               const Source& src,
+               std::vector<const ast::Node*> global_decls)
+    : Base(pid, nid, src), global_declarations_(std::move(global_decls)) {
     for (auto* decl : global_declarations_) {
         if (decl == nullptr) {
             continue;
diff --git a/src/tint/ast/module.h b/src/tint/ast/module.h
index 17cf353..27c62dc 100644
--- a/src/tint/ast/module.h
+++ b/src/tint/ast/module.h
@@ -32,15 +32,17 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    Module(ProgramID pid, const Source& src);
+    Module(ProgramID pid, NodeID nid, const Source& src);
 
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param global_decls the list of global types, functions, and variables, in
     /// the order they were declared in the source program
-    Module(ProgramID pid, const Source& src, std::vector<const Node*> global_decls);
+    Module(ProgramID pid, NodeID nid, const Source& src, std::vector<const Node*> global_decls);
 
     /// Destructor
     ~Module() override;
diff --git a/src/tint/ast/multisampled_texture.cc b/src/tint/ast/multisampled_texture.cc
index 91f8edf..0c44857 100644
--- a/src/tint/ast/multisampled_texture.cc
+++ b/src/tint/ast/multisampled_texture.cc
@@ -21,10 +21,11 @@
 namespace tint::ast {
 
 MultisampledTexture::MultisampledTexture(ProgramID pid,
+                                         NodeID nid,
                                          const Source& src,
                                          TextureDimension d,
                                          const Type* ty)
-    : Base(pid, src, d), type(ty) {
+    : Base(pid, nid, src, d), type(ty) {
     TINT_ASSERT(AST, type);
 }
 
diff --git a/src/tint/ast/multisampled_texture.h b/src/tint/ast/multisampled_texture.h
index 1d95505..6887045 100644
--- a/src/tint/ast/multisampled_texture.h
+++ b/src/tint/ast/multisampled_texture.h
@@ -26,10 +26,15 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param dim the dimensionality of the texture
     /// @param type the data type of the multisampled texture
-    MultisampledTexture(ProgramID pid, const Source& src, TextureDimension dim, const Type* type);
+    MultisampledTexture(ProgramID pid,
+                        NodeID nid,
+                        const Source& src,
+                        TextureDimension dim,
+                        const Type* type);
     /// Move constructor
     MultisampledTexture(MultisampledTexture&&);
     ~MultisampledTexture() override;
diff --git a/src/tint/ast/node.cc b/src/tint/ast/node.cc
index 2368791..ce3a71d 100644
--- a/src/tint/ast/node.cc
+++ b/src/tint/ast/node.cc
@@ -18,7 +18,8 @@
 
 namespace tint::ast {
 
-Node::Node(ProgramID pid, const Source& src) : program_id(pid), source(src) {}
+Node::Node(ProgramID pid, NodeID nid, const Source& src)
+    : program_id(pid), node_id(nid), source(src) {}
 
 Node::Node(Node&&) = default;
 
diff --git a/src/tint/ast/node.h b/src/tint/ast/node.h
index 19d7682..6eaa1e9 100644
--- a/src/tint/ast/node.h
+++ b/src/tint/ast/node.h
@@ -17,6 +17,7 @@
 
 #include <string>
 
+#include "src/tint/ast/node_id.h"
 #include "src/tint/clone_context.h"
 
 namespace tint::ast {
@@ -29,14 +30,18 @@
     /// The identifier of the program that owns this node
     const ProgramID program_id;
 
+    /// The node identifier, unique for the program.
+    const NodeID node_id;
+
     /// The node source data
     const Source source;
 
   protected:
     /// Create a new node
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the input source for the node
-    Node(ProgramID pid, const Source& src);
+    Node(ProgramID pid, NodeID nid, const Source& src);
     /// Move constructor
     Node(Node&&);
 
diff --git a/src/tint/ast/node_id.h b/src/tint/ast/node_id.h
new file mode 100644
index 0000000..79683b0
--- /dev/null
+++ b/src/tint/ast/node_id.h
@@ -0,0 +1,36 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_NODE_ID_H_
+#define SRC_TINT_AST_NODE_ID_H_
+
+#include <stddef.h>
+
+namespace tint::ast {
+
+/// NodeID is a unique node identifier for a given Program.
+/// NodeIDs are sequentially allocated, starting at 0.
+struct NodeID {
+    /// Equality operator
+    /// @param other the other NodeID
+    /// @returns true if the NodeIDs are the same
+    bool operator==(const NodeID& other) const { return value == other.value; }
+
+    /// The numerical value for the node identifier
+    size_t value = 0;
+};
+
+}  // namespace tint::ast
+
+#endif  // SRC_TINT_AST_NODE_ID_H_
diff --git a/src/tint/ast/override.cc b/src/tint/ast/override.cc
index efd048d..b7ee064 100644
--- a/src/tint/ast/override.cc
+++ b/src/tint/ast/override.cc
@@ -21,12 +21,13 @@
 namespace tint::ast {
 
 Override::Override(ProgramID pid,
+                   NodeID nid,
                    const Source& src,
                    const Symbol& sym,
                    const ast::Type* ty,
                    const Expression* ctor,
                    AttributeList attrs)
-    : Base(pid, src, sym, ty, ctor, attrs) {}
+    : Base(pid, nid, src, sym, ty, ctor, attrs) {}
 
 Override::Override(Override&&) = default;
 
diff --git a/src/tint/ast/override.h b/src/tint/ast/override.h
index 98319e5..e97f14c 100644
--- a/src/tint/ast/override.h
+++ b/src/tint/ast/override.h
@@ -31,13 +31,15 @@
 class Override final : public Castable<Override, Variable> {
   public:
     /// Create an 'override' pipeline-overridable constant.
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the variable source
     /// @param sym the variable symbol
     /// @param type the declared variable type
     /// @param constructor the constructor expression
     /// @param attributes the variable attributes
-    Override(ProgramID program_id,
+    Override(ProgramID pid,
+             NodeID nid,
              const Source& source,
              const Symbol& sym,
              const ast::Type* type,
diff --git a/src/tint/ast/parameter.cc b/src/tint/ast/parameter.cc
index ea3c51f..bab2729 100644
--- a/src/tint/ast/parameter.cc
+++ b/src/tint/ast/parameter.cc
@@ -21,11 +21,12 @@
 namespace tint::ast {
 
 Parameter::Parameter(ProgramID pid,
+                     NodeID nid,
                      const Source& src,
                      const Symbol& sym,
                      const ast::Type* ty,
                      AttributeList attrs)
-    : Base(pid, src, sym, ty, nullptr, attrs) {}
+    : Base(pid, nid, src, sym, ty, nullptr, attrs) {}
 
 Parameter::Parameter(Parameter&&) = default;
 
diff --git a/src/tint/ast/parameter.h b/src/tint/ast/parameter.h
index eb4b688..4a9bb93 100644
--- a/src/tint/ast/parameter.h
+++ b/src/tint/ast/parameter.h
@@ -34,12 +34,14 @@
 class Parameter final : public Castable<Parameter, Variable> {
   public:
     /// Create a 'parameter' creation-time value variable.
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the variable source
     /// @param sym the variable symbol
     /// @param type the declared variable type
     /// @param attributes the variable attributes
-    Parameter(ProgramID program_id,
+    Parameter(ProgramID pid,
+              NodeID nid,
               const Source& source,
               const Symbol& sym,
               const ast::Type* type,
diff --git a/src/tint/ast/phony_expression.cc b/src/tint/ast/phony_expression.cc
index a3fd4fd..6bce1bf 100644
--- a/src/tint/ast/phony_expression.cc
+++ b/src/tint/ast/phony_expression.cc
@@ -20,7 +20,8 @@
 
 namespace tint::ast {
 
-PhonyExpression::PhonyExpression(ProgramID pid, const Source& src) : Base(pid, src) {}
+PhonyExpression::PhonyExpression(ProgramID pid, NodeID nid, const Source& src)
+    : Base(pid, nid, src) {}
 
 PhonyExpression::PhonyExpression(PhonyExpression&&) = default;
 
diff --git a/src/tint/ast/phony_expression.h b/src/tint/ast/phony_expression.h
index 4fc32dd..d429a51 100644
--- a/src/tint/ast/phony_expression.h
+++ b/src/tint/ast/phony_expression.h
@@ -25,8 +25,9 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    PhonyExpression(ProgramID pid, const Source& src);
+    PhonyExpression(ProgramID pid, NodeID nid, const Source& src);
     /// Move constructor
     PhonyExpression(PhonyExpression&&);
     ~PhonyExpression() override;
diff --git a/src/tint/ast/pointer.cc b/src/tint/ast/pointer.cc
index 42c3fa9..796fe85 100644
--- a/src/tint/ast/pointer.cc
+++ b/src/tint/ast/pointer.cc
@@ -21,11 +21,12 @@
 namespace tint::ast {
 
 Pointer::Pointer(ProgramID pid,
+                 NodeID nid,
                  const Source& src,
                  const Type* const subtype,
                  ast::StorageClass sc,
                  ast::Access ac)
-    : Base(pid, src), type(subtype), storage_class(sc), access(ac) {}
+    : Base(pid, nid, src), type(subtype), storage_class(sc), access(ac) {}
 
 std::string Pointer::FriendlyName(const SymbolTable& symbols) const {
     std::ostringstream out;
diff --git a/src/tint/ast/pointer.h b/src/tint/ast/pointer.h
index 030e844..61eff88 100644
--- a/src/tint/ast/pointer.h
+++ b/src/tint/ast/pointer.h
@@ -28,11 +28,13 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param subtype the pointee type
     /// @param storage_class the storage class of the pointer
     /// @param access the access control of the pointer
     Pointer(ProgramID pid,
+            NodeID nid,
             const Source& src,
             const Type* const subtype,
             ast::StorageClass storage_class,
diff --git a/src/tint/ast/return_statement.cc b/src/tint/ast/return_statement.cc
index 976c063..459bb72 100644
--- a/src/tint/ast/return_statement.cc
+++ b/src/tint/ast/return_statement.cc
@@ -20,11 +20,14 @@
 
 namespace tint::ast {
 
-ReturnStatement::ReturnStatement(ProgramID pid, const Source& src)
-    : Base(pid, src), value(nullptr) {}
+ReturnStatement::ReturnStatement(ProgramID pid, NodeID nid, const Source& src)
+    : Base(pid, nid, src), value(nullptr) {}
 
-ReturnStatement::ReturnStatement(ProgramID pid, const Source& src, const Expression* val)
-    : Base(pid, src), value(val) {
+ReturnStatement::ReturnStatement(ProgramID pid,
+                                 NodeID nid,
+                                 const Source& src,
+                                 const Expression* val)
+    : Base(pid, nid, src), value(val) {
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, value, program_id);
 }
 
diff --git a/src/tint/ast/return_statement.h b/src/tint/ast/return_statement.h
index 34d8678..571a738 100644
--- a/src/tint/ast/return_statement.h
+++ b/src/tint/ast/return_statement.h
@@ -25,14 +25,16 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    ReturnStatement(ProgramID pid, const Source& src);
+    ReturnStatement(ProgramID pid, NodeID nid, const Source& src);
 
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param value the return value
-    ReturnStatement(ProgramID pid, const Source& src, const Expression* value);
+    ReturnStatement(ProgramID pid, NodeID nid, const Source& src, const Expression* value);
     /// Move constructor
     ReturnStatement(ReturnStatement&&);
     ~ReturnStatement() override;
diff --git a/src/tint/ast/sampled_texture.cc b/src/tint/ast/sampled_texture.cc
index 9c4cea6..b8dfd61 100644
--- a/src/tint/ast/sampled_texture.cc
+++ b/src/tint/ast/sampled_texture.cc
@@ -20,8 +20,12 @@
 
 namespace tint::ast {
 
-SampledTexture::SampledTexture(ProgramID pid, const Source& src, TextureDimension d, const Type* ty)
-    : Base(pid, src, d), type(ty) {
+SampledTexture::SampledTexture(ProgramID pid,
+                               NodeID nid,
+                               const Source& src,
+                               TextureDimension d,
+                               const Type* ty)
+    : Base(pid, nid, src, d), type(ty) {
     TINT_ASSERT(AST, type);
 }
 
diff --git a/src/tint/ast/sampled_texture.h b/src/tint/ast/sampled_texture.h
index f68fccf..1f33af3 100644
--- a/src/tint/ast/sampled_texture.h
+++ b/src/tint/ast/sampled_texture.h
@@ -26,10 +26,15 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param dim the dimensionality of the texture
     /// @param type the data type of the sampled texture
-    SampledTexture(ProgramID pid, const Source& src, TextureDimension dim, const Type* type);
+    SampledTexture(ProgramID pid,
+                   NodeID nid,
+                   const Source& src,
+                   TextureDimension dim,
+                   const Type* type);
     /// Move constructor
     SampledTexture(SampledTexture&&);
     ~SampledTexture() override;
diff --git a/src/tint/ast/sampler.cc b/src/tint/ast/sampler.cc
index 5d88bf8..5237380 100644
--- a/src/tint/ast/sampler.cc
+++ b/src/tint/ast/sampler.cc
@@ -32,7 +32,8 @@
     return out;
 }
 
-Sampler::Sampler(ProgramID pid, const Source& src, SamplerKind k) : Base(pid, src), kind(k) {}
+Sampler::Sampler(ProgramID pid, NodeID nid, const Source& src, SamplerKind k)
+    : Base(pid, nid, src), kind(k) {}
 
 Sampler::Sampler(Sampler&&) = default;
 
diff --git a/src/tint/ast/sampler.h b/src/tint/ast/sampler.h
index 067fc38..bcdf751 100644
--- a/src/tint/ast/sampler.h
+++ b/src/tint/ast/sampler.h
@@ -39,9 +39,10 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param kind the kind of sampler
-    Sampler(ProgramID pid, const Source& src, SamplerKind kind);
+    Sampler(ProgramID pid, NodeID nid, const Source& src, SamplerKind kind);
     /// Move constructor
     Sampler(Sampler&&);
     ~Sampler() override;
diff --git a/src/tint/ast/stage_attribute.cc b/src/tint/ast/stage_attribute.cc
index 51cfe8c..c700950 100644
--- a/src/tint/ast/stage_attribute.cc
+++ b/src/tint/ast/stage_attribute.cc
@@ -22,8 +22,8 @@
 
 namespace tint::ast {
 
-StageAttribute::StageAttribute(ProgramID pid, const Source& src, PipelineStage s)
-    : Base(pid, src), stage(s) {}
+StageAttribute::StageAttribute(ProgramID pid, NodeID nid, const Source& src, PipelineStage s)
+    : Base(pid, nid, src), stage(s) {}
 
 StageAttribute::~StageAttribute() = default;
 
diff --git a/src/tint/ast/stage_attribute.h b/src/tint/ast/stage_attribute.h
index a447d1f..0bf9d9e 100644
--- a/src/tint/ast/stage_attribute.h
+++ b/src/tint/ast/stage_attribute.h
@@ -26,10 +26,11 @@
 class StageAttribute final : public Castable<StageAttribute, Attribute> {
   public:
     /// constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param stage the pipeline stage
     /// @param source the source of this attribute
-    StageAttribute(ProgramID program_id, const Source& source, PipelineStage stage);
+    StageAttribute(ProgramID pid, NodeID nid, const Source& source, PipelineStage stage);
     ~StageAttribute() override;
 
     /// @returns the WGSL name for the attribute
diff --git a/src/tint/ast/statement.cc b/src/tint/ast/statement.cc
index 12a1cc9..6acfff3 100644
--- a/src/tint/ast/statement.cc
+++ b/src/tint/ast/statement.cc
@@ -30,7 +30,7 @@
 
 namespace tint::ast {
 
-Statement::Statement(ProgramID pid, const Source& src) : Base(pid, src) {}
+Statement::Statement(ProgramID pid, NodeID nid, const Source& src) : Base(pid, nid, src) {}
 
 Statement::Statement(Statement&&) = default;
 
diff --git a/src/tint/ast/statement.h b/src/tint/ast/statement.h
index 94de247..e3a96d1 100644
--- a/src/tint/ast/statement.h
+++ b/src/tint/ast/statement.h
@@ -32,8 +32,9 @@
   protected:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of the expression
-    Statement(ProgramID pid, const Source& src);
+    Statement(ProgramID pid, NodeID nid, const Source& src);
     /// Move constructor
     Statement(Statement&&);
 };
diff --git a/src/tint/ast/storage_texture.cc b/src/tint/ast/storage_texture.cc
index ccc250d..d75bb0f 100644
--- a/src/tint/ast/storage_texture.cc
+++ b/src/tint/ast/storage_texture.cc
@@ -83,12 +83,13 @@
 }
 
 StorageTexture::StorageTexture(ProgramID pid,
+                               NodeID nid,
                                const Source& src,
                                TextureDimension d,
                                TexelFormat fmt,
                                const Type* subtype,
                                Access ac)
-    : Base(pid, src, d), format(fmt), type(subtype), access(ac) {}
+    : Base(pid, nid, src, d), format(fmt), type(subtype), access(ac) {}
 
 StorageTexture::StorageTexture(StorageTexture&&) = default;
 
diff --git a/src/tint/ast/storage_texture.h b/src/tint/ast/storage_texture.h
index 3cf779e..ac7fee1 100644
--- a/src/tint/ast/storage_texture.h
+++ b/src/tint/ast/storage_texture.h
@@ -53,12 +53,14 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param dim the dimensionality of the texture
     /// @param format the image format of the texture
     /// @param subtype the storage subtype. Use SubtypeFor() to calculate this.
     /// @param access_control the access control for the texture.
     StorageTexture(ProgramID pid,
+                   NodeID nid,
                    const Source& src,
                    TextureDimension dim,
                    TexelFormat format,
diff --git a/src/tint/ast/stride_attribute.cc b/src/tint/ast/stride_attribute.cc
index 14a0733..408ee44 100644
--- a/src/tint/ast/stride_attribute.cc
+++ b/src/tint/ast/stride_attribute.cc
@@ -22,8 +22,8 @@
 
 namespace tint::ast {
 
-StrideAttribute::StrideAttribute(ProgramID pid, const Source& src, uint32_t s)
-    : Base(pid, src), stride(s) {}
+StrideAttribute::StrideAttribute(ProgramID pid, NodeID nid, const Source& src, uint32_t s)
+    : Base(pid, nid, src), stride(s) {}
 
 StrideAttribute::~StrideAttribute() = default;
 
diff --git a/src/tint/ast/stride_attribute.h b/src/tint/ast/stride_attribute.h
index 4315f21..9014677 100644
--- a/src/tint/ast/stride_attribute.h
+++ b/src/tint/ast/stride_attribute.h
@@ -28,9 +28,10 @@
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param stride the stride value
-    StrideAttribute(ProgramID pid, const Source& src, uint32_t stride);
+    StrideAttribute(ProgramID pid, NodeID nid, const Source& src, uint32_t stride);
     ~StrideAttribute() override;
 
     /// @returns the WGSL name for the attribute
diff --git a/src/tint/ast/struct.cc b/src/tint/ast/struct.cc
index 19a30de..326718a 100644
--- a/src/tint/ast/struct.cc
+++ b/src/tint/ast/struct.cc
@@ -22,8 +22,13 @@
 
 namespace tint::ast {
 
-Struct::Struct(ProgramID pid, const Source& src, Symbol n, StructMemberList m, AttributeList attrs)
-    : Base(pid, src, n), members(std::move(m)), attributes(std::move(attrs)) {
+Struct::Struct(ProgramID pid,
+               NodeID nid,
+               const Source& src,
+               Symbol n,
+               StructMemberList m,
+               AttributeList attrs)
+    : Base(pid, nid, src, n), members(std::move(m)), attributes(std::move(attrs)) {
     for (auto* mem : members) {
         TINT_ASSERT(AST, mem);
         TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, mem, program_id);
diff --git a/src/tint/ast/struct.h b/src/tint/ast/struct.h
index 5c28b4c..5d55b8c 100644
--- a/src/tint/ast/struct.h
+++ b/src/tint/ast/struct.h
@@ -29,11 +29,13 @@
   public:
     /// Create a new struct statement
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node for the import statement
     /// @param name The name of the structure
     /// @param members The struct members
     /// @param attributes The struct attributes
     Struct(ProgramID pid,
+           NodeID nid,
            const Source& src,
            Symbol name,
            StructMemberList members,
diff --git a/src/tint/ast/struct_member.cc b/src/tint/ast/struct_member.cc
index 6113484..72acd33 100644
--- a/src/tint/ast/struct_member.cc
+++ b/src/tint/ast/struct_member.cc
@@ -21,11 +21,12 @@
 namespace tint::ast {
 
 StructMember::StructMember(ProgramID pid,
+                           NodeID nid,
                            const Source& src,
                            const Symbol& sym,
                            const ast::Type* ty,
                            AttributeList attrs)
-    : Base(pid, src), symbol(sym), type(ty), attributes(std::move(attrs)) {
+    : Base(pid, nid, src), symbol(sym), type(ty), attributes(std::move(attrs)) {
     TINT_ASSERT(AST, type);
     TINT_ASSERT(AST, symbol.IsValid());
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, symbol, program_id);
diff --git a/src/tint/ast/struct_member.h b/src/tint/ast/struct_member.h
index 022a34c..39c8532 100644
--- a/src/tint/ast/struct_member.h
+++ b/src/tint/ast/struct_member.h
@@ -32,11 +32,13 @@
   public:
     /// Create a new struct member statement
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node for the struct member statement
     /// @param sym The struct member symbol
     /// @param type The struct member type
     /// @param attributes The struct member attributes
     StructMember(ProgramID pid,
+                 NodeID nid,
                  const Source& src,
                  const Symbol& sym,
                  const ast::Type* type,
diff --git a/src/tint/ast/struct_member_align_attribute.cc b/src/tint/ast/struct_member_align_attribute.cc
index f586e7e..d8ed4fc 100644
--- a/src/tint/ast/struct_member_align_attribute.cc
+++ b/src/tint/ast/struct_member_align_attribute.cc
@@ -23,8 +23,11 @@
 
 namespace tint::ast {
 
-StructMemberAlignAttribute::StructMemberAlignAttribute(ProgramID pid, const Source& src, uint32_t a)
-    : Base(pid, src), align(a) {}
+StructMemberAlignAttribute::StructMemberAlignAttribute(ProgramID pid,
+                                                       NodeID nid,
+                                                       const Source& src,
+                                                       uint32_t a)
+    : Base(pid, nid, src), align(a) {}
 
 StructMemberAlignAttribute::~StructMemberAlignAttribute() = default;
 
diff --git a/src/tint/ast/struct_member_align_attribute.h b/src/tint/ast/struct_member_align_attribute.h
index 10a6507..efff21b 100644
--- a/src/tint/ast/struct_member_align_attribute.h
+++ b/src/tint/ast/struct_member_align_attribute.h
@@ -27,9 +27,10 @@
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param align the align value
-    StructMemberAlignAttribute(ProgramID pid, const Source& src, uint32_t align);
+    StructMemberAlignAttribute(ProgramID pid, NodeID nid, const Source& src, uint32_t align);
     ~StructMemberAlignAttribute() override;
 
     /// @returns the WGSL name for the attribute
diff --git a/src/tint/ast/struct_member_offset_attribute.cc b/src/tint/ast/struct_member_offset_attribute.cc
index 0a33127..48d7333 100644
--- a/src/tint/ast/struct_member_offset_attribute.cc
+++ b/src/tint/ast/struct_member_offset_attribute.cc
@@ -23,9 +23,10 @@
 namespace tint::ast {
 
 StructMemberOffsetAttribute::StructMemberOffsetAttribute(ProgramID pid,
+                                                         NodeID nid,
                                                          const Source& src,
                                                          uint32_t o)
-    : Base(pid, src), offset(o) {}
+    : Base(pid, nid, src), offset(o) {}
 
 StructMemberOffsetAttribute::~StructMemberOffsetAttribute() = default;
 
diff --git a/src/tint/ast/struct_member_offset_attribute.h b/src/tint/ast/struct_member_offset_attribute.h
index 92cc68e..790927e 100644
--- a/src/tint/ast/struct_member_offset_attribute.h
+++ b/src/tint/ast/struct_member_offset_attribute.h
@@ -35,9 +35,10 @@
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param offset the offset value
-    StructMemberOffsetAttribute(ProgramID pid, const Source& src, uint32_t offset);
+    StructMemberOffsetAttribute(ProgramID pid, NodeID nid, const Source& src, uint32_t offset);
     ~StructMemberOffsetAttribute() override;
 
     /// @returns the WGSL name for the attribute
diff --git a/src/tint/ast/struct_member_size_attribute.cc b/src/tint/ast/struct_member_size_attribute.cc
index a7f291b..3919078 100644
--- a/src/tint/ast/struct_member_size_attribute.cc
+++ b/src/tint/ast/struct_member_size_attribute.cc
@@ -23,8 +23,11 @@
 
 namespace tint::ast {
 
-StructMemberSizeAttribute::StructMemberSizeAttribute(ProgramID pid, const Source& src, uint32_t sz)
-    : Base(pid, src), size(sz) {}
+StructMemberSizeAttribute::StructMemberSizeAttribute(ProgramID pid,
+                                                     NodeID nid,
+                                                     const Source& src,
+                                                     uint32_t sz)
+    : Base(pid, nid, src), size(sz) {}
 
 StructMemberSizeAttribute::~StructMemberSizeAttribute() = default;
 
diff --git a/src/tint/ast/struct_member_size_attribute.h b/src/tint/ast/struct_member_size_attribute.h
index 0c4ddd6..5649e2e 100644
--- a/src/tint/ast/struct_member_size_attribute.h
+++ b/src/tint/ast/struct_member_size_attribute.h
@@ -27,9 +27,10 @@
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param size the size value
-    StructMemberSizeAttribute(ProgramID pid, const Source& src, uint32_t size);
+    StructMemberSizeAttribute(ProgramID pid, NodeID nid, const Source& src, uint32_t size);
     ~StructMemberSizeAttribute() override;
 
     /// @returns the WGSL name for the attribute
diff --git a/src/tint/ast/struct_test.cc b/src/tint/ast/struct_test.cc
index 895d501..a94ceb3 100644
--- a/src/tint/ast/struct_test.cc
+++ b/src/tint/ast/struct_test.cc
@@ -49,7 +49,7 @@
 TEST_F(AstStructTest, Creation_WithAttributes) {
     auto name = Sym("s");
     AttributeList attrs;
-    attrs.push_back(ASTNodes().Create<SpirvBlockAttribute>(ID()));
+    attrs.push_back(ASTNodes().Create<SpirvBlockAttribute>(ID(), AllocateNodeID()));
 
     auto* s = create<Struct>(name, StructMemberList{Member("a", ty.i32())}, attrs);
     EXPECT_EQ(s->name, name);
@@ -64,10 +64,10 @@
 
 TEST_F(AstStructTest, CreationWithSourceAndAttributes) {
     auto name = Sym("s");
-    auto* s =
-        create<Struct>(Source{Source::Range{Source::Location{27, 4}, Source::Location{27, 8}}},
-                       name, StructMemberList{Member("a", ty.i32())},
-                       AttributeList{ASTNodes().Create<SpirvBlockAttribute>(ID())});
+    auto* s = create<Struct>(
+        Source{Source::Range{Source::Location{27, 4}, Source::Location{27, 8}}}, name,
+        StructMemberList{Member("a", ty.i32())},
+        AttributeList{ASTNodes().Create<SpirvBlockAttribute>(ID(), AllocateNodeID())});
     EXPECT_EQ(s->name, name);
     EXPECT_EQ(s->members.size(), 1u);
     ASSERT_EQ(s->attributes.size(), 1u);
@@ -115,7 +115,8 @@
             ProgramBuilder b1;
             ProgramBuilder b2;
             b1.create<Struct>(b1.Sym("S"), StructMemberList{b1.Member("a", b1.ty.i32())},
-                              AttributeList{b2.ASTNodes().Create<SpirvBlockAttribute>(b2.ID())});
+                              AttributeList{b2.ASTNodes().Create<SpirvBlockAttribute>(
+                                  b2.ID(), b2.AllocateNodeID())});
         },
         "internal compiler error");
 }
diff --git a/src/tint/ast/switch_statement.cc b/src/tint/ast/switch_statement.cc
index 08095a1..7abf0c0 100644
--- a/src/tint/ast/switch_statement.cc
+++ b/src/tint/ast/switch_statement.cc
@@ -21,10 +21,11 @@
 namespace tint::ast {
 
 SwitchStatement::SwitchStatement(ProgramID pid,
+                                 NodeID nid,
                                  const Source& src,
                                  const Expression* cond,
                                  CaseStatementList b)
-    : Base(pid, src), condition(cond), body(b) {
+    : Base(pid, nid, src), condition(cond), body(b) {
     TINT_ASSERT(AST, condition);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, condition, program_id);
     for (auto* stmt : body) {
diff --git a/src/tint/ast/switch_statement.h b/src/tint/ast/switch_statement.h
index 5ac13b7..c13ac88 100644
--- a/src/tint/ast/switch_statement.h
+++ b/src/tint/ast/switch_statement.h
@@ -25,10 +25,12 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param condition the switch condition
     /// @param body the switch body
     SwitchStatement(ProgramID pid,
+                    NodeID nid,
                     const Source& src,
                     const Expression* condition,
                     CaseStatementList body);
diff --git a/src/tint/ast/texture.cc b/src/tint/ast/texture.cc
index 27eb094..f3c01df 100644
--- a/src/tint/ast/texture.cc
+++ b/src/tint/ast/texture.cc
@@ -77,7 +77,8 @@
     return 0;
 }
 
-Texture::Texture(ProgramID pid, const Source& src, TextureDimension d) : Base(pid, src), dim(d) {}
+Texture::Texture(ProgramID pid, NodeID nid, const Source& src, TextureDimension d)
+    : Base(pid, nid, src), dim(d) {}
 
 Texture::Texture(Texture&&) = default;
 
diff --git a/src/tint/ast/texture.h b/src/tint/ast/texture.h
index 9a4199b..fcfa334 100644
--- a/src/tint/ast/texture.h
+++ b/src/tint/ast/texture.h
@@ -65,9 +65,10 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param dim the dimensionality of the texture
-    Texture(ProgramID pid, const Source& src, TextureDimension dim);
+    Texture(ProgramID pid, NodeID nid, const Source& src, TextureDimension dim);
     /// Move constructor
     Texture(Texture&&);
     ~Texture() override;
diff --git a/src/tint/ast/type.h b/src/tint/ast/type.h
index 4fee565..4f1f276 100644
--- a/src/tint/ast/type.h
+++ b/src/tint/ast/type.h
@@ -42,8 +42,9 @@
   protected:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    Type(ProgramID pid, const Source& src);
+    Type(ProgramID pid, NodeID nid, const Source& src);
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/ast/type_decl.cc b/src/tint/ast/type_decl.cc
index a1a0605..0b76524 100644
--- a/src/tint/ast/type_decl.cc
+++ b/src/tint/ast/type_decl.cc
@@ -20,7 +20,8 @@
 
 namespace tint::ast {
 
-TypeDecl::TypeDecl(ProgramID pid, const Source& src, Symbol n) : Base(pid, src), name(n) {
+TypeDecl::TypeDecl(ProgramID pid, NodeID nid, const Source& src, Symbol n)
+    : Base(pid, nid, src), name(n) {
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, name, program_id);
 }
 
diff --git a/src/tint/ast/type_decl.h b/src/tint/ast/type_decl.h
index 2b8487a..e3266c6 100644
--- a/src/tint/ast/type_decl.h
+++ b/src/tint/ast/type_decl.h
@@ -26,9 +26,10 @@
   public:
     /// Create a new struct statement
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node for the import statement
     /// @param name The name of the structure
-    TypeDecl(ProgramID pid, const Source& src, Symbol name);
+    TypeDecl(ProgramID pid, NodeID nid, const Source& src, Symbol name);
     /// Move constructor
     TypeDecl(TypeDecl&&);
 
diff --git a/src/tint/ast/type_name.cc b/src/tint/ast/type_name.cc
index 8eb7a1a..852abf8 100644
--- a/src/tint/ast/type_name.cc
+++ b/src/tint/ast/type_name.cc
@@ -20,7 +20,8 @@
 
 namespace tint::ast {
 
-TypeName::TypeName(ProgramID pid, const Source& src, Symbol n) : Base(pid, src), name(n) {}
+TypeName::TypeName(ProgramID pid, NodeID nid, const Source& src, Symbol n)
+    : Base(pid, nid, src), name(n) {}
 
 TypeName::~TypeName() = default;
 
diff --git a/src/tint/ast/type_name.h b/src/tint/ast/type_name.h
index 3bb556a..ed7e2f2 100644
--- a/src/tint/ast/type_name.h
+++ b/src/tint/ast/type_name.h
@@ -26,9 +26,10 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param name the type name
-    TypeName(ProgramID pid, const Source& src, Symbol name);
+    TypeName(ProgramID pid, NodeID nid, const Source& src, Symbol name);
     /// Move constructor
     TypeName(TypeName&&);
     /// Destructor
diff --git a/src/tint/ast/u32.cc b/src/tint/ast/u32.cc
index ac9c490..c99dc4f 100644
--- a/src/tint/ast/u32.cc
+++ b/src/tint/ast/u32.cc
@@ -20,7 +20,7 @@
 
 namespace tint::ast {
 
-U32::U32(ProgramID pid, const Source& src) : Base(pid, src) {}
+U32::U32(ProgramID pid, NodeID nid, const Source& src) : Base(pid, nid, src) {}
 
 U32::~U32() = default;
 
diff --git a/src/tint/ast/u32.h b/src/tint/ast/u32.h
index 8ede11c..9237278 100644
--- a/src/tint/ast/u32.h
+++ b/src/tint/ast/u32.h
@@ -26,8 +26,9 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    U32(ProgramID pid, const Source& src);
+    U32(ProgramID pid, NodeID nid, const Source& src);
     /// Move constructor
     U32(U32&&);
     ~U32() override;
diff --git a/src/tint/ast/unary_op_expression.cc b/src/tint/ast/unary_op_expression.cc
index 80e4e90..eec69a0 100644
--- a/src/tint/ast/unary_op_expression.cc
+++ b/src/tint/ast/unary_op_expression.cc
@@ -21,10 +21,11 @@
 namespace tint::ast {
 
 UnaryOpExpression::UnaryOpExpression(ProgramID pid,
+                                     NodeID nid,
                                      const Source& src,
                                      UnaryOp o,
                                      const Expression* e)
-    : Base(pid, src), op(o), expr(e) {
+    : Base(pid, nid, src), op(o), expr(e) {
     TINT_ASSERT(AST, expr);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, expr, program_id);
 }
diff --git a/src/tint/ast/unary_op_expression.h b/src/tint/ast/unary_op_expression.h
index 22093fb..a5c2be9 100644
--- a/src/tint/ast/unary_op_expression.h
+++ b/src/tint/ast/unary_op_expression.h
@@ -24,11 +24,13 @@
 class UnaryOpExpression final : public Castable<UnaryOpExpression, Expression> {
   public:
     /// Constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the unary op expression source
     /// @param op the op
     /// @param expr the expr
-    UnaryOpExpression(ProgramID program_id,
+    UnaryOpExpression(ProgramID pid,
+                      NodeID nid,
                       const Source& source,
                       UnaryOp op,
                       const Expression* expr);
diff --git a/src/tint/ast/var.cc b/src/tint/ast/var.cc
index 854503e..763949f 100644
--- a/src/tint/ast/var.cc
+++ b/src/tint/ast/var.cc
@@ -21,6 +21,7 @@
 namespace tint::ast {
 
 Var::Var(ProgramID pid,
+         NodeID nid,
          const Source& src,
          const Symbol& sym,
          const ast::Type* ty,
@@ -28,7 +29,7 @@
          Access access,
          const Expression* ctor,
          AttributeList attrs)
-    : Base(pid, src, sym, ty, ctor, attrs),
+    : Base(pid, nid, src, sym, ty, ctor, attrs),
       declared_storage_class(storage_class),
       declared_access(access) {}
 
diff --git a/src/tint/ast/var.h b/src/tint/ast/var.h
index 565ebbb..cd12ff4 100644
--- a/src/tint/ast/var.h
+++ b/src/tint/ast/var.h
@@ -42,7 +42,8 @@
 class Var final : public Castable<Var, Variable> {
   public:
     /// Create a 'var' variable
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the variable source
     /// @param sym the variable symbol
     /// @param type the declared variable type
@@ -50,7 +51,8 @@
     /// @param declared_access the declared access control
     /// @param constructor the constructor expression
     /// @param attributes the variable attributes
-    Var(ProgramID program_id,
+    Var(ProgramID pid,
+        NodeID nid,
         const Source& source,
         const Symbol& sym,
         const ast::Type* type,
diff --git a/src/tint/ast/variable.cc b/src/tint/ast/variable.cc
index a9ae829..b16b2aa 100644
--- a/src/tint/ast/variable.cc
+++ b/src/tint/ast/variable.cc
@@ -21,12 +21,13 @@
 namespace tint::ast {
 
 Variable::Variable(ProgramID pid,
+                   NodeID nid,
                    const Source& src,
                    const Symbol& sym,
                    const ast::Type* ty,
                    const Expression* ctor,
                    AttributeList attrs)
-    : Base(pid, src), symbol(sym), type(ty), constructor(ctor), attributes(std::move(attrs)) {
+    : Base(pid, nid, src), symbol(sym), type(ty), constructor(ctor), attributes(std::move(attrs)) {
     TINT_ASSERT(AST, symbol.IsValid());
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, symbol, program_id);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, constructor, program_id);
diff --git a/src/tint/ast/variable.h b/src/tint/ast/variable.h
index 631fbe5..3031318 100644
--- a/src/tint/ast/variable.h
+++ b/src/tint/ast/variable.h
@@ -54,13 +54,15 @@
 class Variable : public Castable<Variable, Node> {
   public:
     /// Constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the variable source
     /// @param sym the variable symbol
     /// @param type the declared variable type
     /// @param constructor the constructor expression
     /// @param attributes the variable attributes
-    Variable(ProgramID program_id,
+    Variable(ProgramID pid,
+             NodeID nid,
              const Source& source,
              const Symbol& sym,
              const ast::Type* type,
diff --git a/src/tint/ast/variable_decl_statement.cc b/src/tint/ast/variable_decl_statement.cc
index fdde149..79ee926 100644
--- a/src/tint/ast/variable_decl_statement.cc
+++ b/src/tint/ast/variable_decl_statement.cc
@@ -20,8 +20,11 @@
 
 namespace tint::ast {
 
-VariableDeclStatement::VariableDeclStatement(ProgramID pid, const Source& src, const Variable* var)
-    : Base(pid, src), variable(var) {
+VariableDeclStatement::VariableDeclStatement(ProgramID pid,
+                                             NodeID nid,
+                                             const Source& src,
+                                             const Variable* var)
+    : Base(pid, nid, src), variable(var) {
     TINT_ASSERT(AST, variable);
     TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, variable, program_id);
 }
diff --git a/src/tint/ast/variable_decl_statement.h b/src/tint/ast/variable_decl_statement.h
index 3f3ae27..b71d0b2 100644
--- a/src/tint/ast/variable_decl_statement.h
+++ b/src/tint/ast/variable_decl_statement.h
@@ -24,10 +24,14 @@
 class VariableDeclStatement final : public Castable<VariableDeclStatement, Statement> {
   public:
     /// Constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the variable statement source
     /// @param variable the variable
-    VariableDeclStatement(ProgramID program_id, const Source& source, const Variable* variable);
+    VariableDeclStatement(ProgramID pid,
+                          NodeID nid,
+                          const Source& source,
+                          const Variable* variable);
     /// Move constructor
     VariableDeclStatement(VariableDeclStatement&&);
     ~VariableDeclStatement() override;
diff --git a/src/tint/ast/vector.cc b/src/tint/ast/vector.cc
index 43478df..d49da33 100644
--- a/src/tint/ast/vector.cc
+++ b/src/tint/ast/vector.cc
@@ -20,8 +20,8 @@
 
 namespace tint::ast {
 
-Vector::Vector(ProgramID pid, Source const& src, const Type* subtype, uint32_t w)
-    : Base(pid, src), type(subtype), width(w) {
+Vector::Vector(ProgramID pid, NodeID nid, Source const& src, const Type* subtype, uint32_t w)
+    : Base(pid, nid, 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);
diff --git a/src/tint/ast/vector.h b/src/tint/ast/vector.h
index 6b2d914..111602de 100644
--- a/src/tint/ast/vector.h
+++ b/src/tint/ast/vector.h
@@ -26,12 +26,13 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param subtype the declared type of the vector components. May be null
     ///        for vector constructors, where the element type will be inferred
     ///        from the constructor arguments
     /// @param width the number of elements in the vector
-    Vector(ProgramID pid, Source const& src, const Type* subtype, uint32_t width);
+    Vector(ProgramID pid, NodeID nid, Source const& src, const Type* subtype, uint32_t width);
     /// Move constructor
     Vector(Vector&&);
     ~Vector() override;
diff --git a/src/tint/ast/void.cc b/src/tint/ast/void.cc
index 5cc8963..ead89ef 100644
--- a/src/tint/ast/void.cc
+++ b/src/tint/ast/void.cc
@@ -20,7 +20,7 @@
 
 namespace tint::ast {
 
-Void::Void(ProgramID pid, const Source& src) : Base(pid, src) {}
+Void::Void(ProgramID pid, NodeID nid, const Source& src) : Base(pid, nid, src) {}
 
 Void::Void(Void&&) = default;
 
diff --git a/src/tint/ast/void.h b/src/tint/ast/void.h
index 33f5b5b..dba20f1 100644
--- a/src/tint/ast/void.h
+++ b/src/tint/ast/void.h
@@ -26,8 +26,9 @@
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
-    Void(ProgramID pid, const Source& src);
+    Void(ProgramID pid, NodeID nid, const Source& src);
     /// Move constructor
     Void(Void&&);
     ~Void() override;
diff --git a/src/tint/ast/while_statement.cc b/src/tint/ast/while_statement.cc
index 3666baf..160af4b 100644
--- a/src/tint/ast/while_statement.cc
+++ b/src/tint/ast/while_statement.cc
@@ -21,10 +21,11 @@
 namespace tint::ast {
 
 WhileStatement::WhileStatement(ProgramID pid,
+                               NodeID nid,
                                const Source& src,
                                const Expression* cond,
                                const BlockStatement* b)
-    : Base(pid, src), condition(cond), body(b) {
+    : Base(pid, nid, src), condition(cond), body(b) {
     TINT_ASSERT(AST, cond);
     TINT_ASSERT(AST, body);
 
diff --git a/src/tint/ast/while_statement.h b/src/tint/ast/while_statement.h
index 9a7a6b0..4e8dd7e 100644
--- a/src/tint/ast/while_statement.h
+++ b/src/tint/ast/while_statement.h
@@ -25,12 +25,14 @@
 class WhileStatement final : public Castable<WhileStatement, Statement> {
   public:
     /// Constructor
-    /// @param program_id the identifier of the program that owns this node
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param source the for loop statement source
     /// @param condition the optional loop condition expression
     /// @param body the loop body
-    WhileStatement(ProgramID program_id,
-                   Source const& source,
+    WhileStatement(ProgramID pid,
+                   NodeID nid,
+                   const Source& source,
                    const Expression* condition,
                    const BlockStatement* body);
     /// Move constructor
diff --git a/src/tint/ast/workgroup_attribute.cc b/src/tint/ast/workgroup_attribute.cc
index 74ecdbe..7cb67dc 100644
--- a/src/tint/ast/workgroup_attribute.cc
+++ b/src/tint/ast/workgroup_attribute.cc
@@ -23,11 +23,12 @@
 namespace tint::ast {
 
 WorkgroupAttribute::WorkgroupAttribute(ProgramID pid,
+                                       NodeID nid,
                                        const Source& src,
                                        const ast::Expression* x_,
                                        const ast::Expression* y_,
                                        const ast::Expression* z_)
-    : Base(pid, src), x(x_), y(y_), z(z_) {}
+    : Base(pid, nid, src), x(x_), y(y_), z(z_) {}
 
 WorkgroupAttribute::~WorkgroupAttribute() = default;
 
diff --git a/src/tint/ast/workgroup_attribute.h b/src/tint/ast/workgroup_attribute.h
index 536ce15..e27e77e 100644
--- a/src/tint/ast/workgroup_attribute.h
+++ b/src/tint/ast/workgroup_attribute.h
@@ -32,11 +32,13 @@
   public:
     /// constructor
     /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param x the workgroup x dimension expression
     /// @param y the optional workgroup y dimension expression
     /// @param z the optional workgroup z dimension expression
     WorkgroupAttribute(ProgramID pid,
+                       NodeID nid,
                        const Source& src,
                        const ast::Expression* x,
                        const ast::Expression* y = nullptr,
diff --git a/src/tint/program.cc b/src/tint/program.cc
index 1afbd46..6109c20 100644
--- a/src/tint/program.cc
+++ b/src/tint/program.cc
@@ -35,6 +35,7 @@
 
 Program::Program(Program&& program)
     : id_(std::move(program.id_)),
+      highest_node_id_(std::move(program.highest_node_id_)),
       types_(std::move(program.types_)),
       ast_nodes_(std::move(program.ast_nodes_)),
       sem_nodes_(std::move(program.sem_nodes_)),
@@ -50,6 +51,7 @@
 
 Program::Program(ProgramBuilder&& builder) {
     id_ = builder.ID();
+    highest_node_id_ = builder.LastAllocatedNodeID();
 
     is_valid_ = builder.IsValid();
     if (builder.ResolveOnBuild() && builder.IsValid()) {
@@ -85,6 +87,7 @@
     program.moved_ = true;
     moved_ = false;
     id_ = std::move(program.id_);
+    highest_node_id_ = std::move(program.highest_node_id_);
     types_ = std::move(program.types_);
     ast_nodes_ = std::move(program.ast_nodes_);
     sem_nodes_ = std::move(program.sem_nodes_);
diff --git a/src/tint/program.h b/src/tint/program.h
index 5fd31dd..ea88470 100644
--- a/src/tint/program.h
+++ b/src/tint/program.h
@@ -69,6 +69,9 @@
     /// @returns the unique identifier for this program
     ProgramID ID() const { return id_; }
 
+    /// @returns the last allocated (numerically highest) AST node identifier.
+    ast::NodeID HighestASTNodeID() const { return highest_node_id_; }
+
     /// @returns a reference to the program's types
     const sem::Manager& Types() const {
         AssertNotMoved();
@@ -161,6 +164,7 @@
     void AssertNotMoved() const;
 
     ProgramID id_;
+    ast::NodeID highest_node_id_;
     sem::Manager types_;
     ASTNodeAllocator ast_nodes_;
     SemNodeAllocator sem_nodes_;
diff --git a/src/tint/program_builder.cc b/src/tint/program_builder.cc
index aed6ec8..19f20b4 100644
--- a/src/tint/program_builder.cc
+++ b/src/tint/program_builder.cc
@@ -29,14 +29,16 @@
 ProgramBuilder::VarOptionals::~VarOptionals() = default;
 
 ProgramBuilder::ProgramBuilder()
-    : id_(ProgramID::New()), ast_(ast_nodes_.Create<ast::Module>(id_, Source{})) {}
+    : id_(ProgramID::New()),
+      ast_(ast_nodes_.Create<ast::Module>(id_, AllocateNodeID(), Source{})) {}
 
 ProgramBuilder::ProgramBuilder(ProgramBuilder&& rhs)
     : id_(std::move(rhs.id_)),
+      last_ast_node_id_(std::move(rhs.last_ast_node_id_)),
       types_(std::move(rhs.types_)),
       ast_nodes_(std::move(rhs.ast_nodes_)),
       sem_nodes_(std::move(rhs.sem_nodes_)),
-      ast_(rhs.ast_),
+      ast_(std::move(rhs.ast_)),
       sem_(std::move(rhs.sem_)),
       symbols_(std::move(rhs.symbols_)),
       diagnostics_(std::move(rhs.diagnostics_)) {
@@ -49,10 +51,11 @@
     rhs.MarkAsMoved();
     AssertNotMoved();
     id_ = std::move(rhs.id_);
+    last_ast_node_id_ = std::move(rhs.last_ast_node_id_);
     types_ = std::move(rhs.types_);
     ast_nodes_ = std::move(rhs.ast_nodes_);
     sem_nodes_ = std::move(rhs.sem_nodes_);
-    ast_ = rhs.ast_;
+    ast_ = std::move(rhs.ast_);
     sem_ = std::move(rhs.sem_);
     symbols_ = std::move(rhs.symbols_);
     diagnostics_ = std::move(rhs.diagnostics_);
@@ -63,6 +66,7 @@
 ProgramBuilder ProgramBuilder::Wrap(const Program* program) {
     ProgramBuilder builder;
     builder.id_ = program->ID();
+    builder.last_ast_node_id_ = program->HighestASTNodeID();
     builder.types_ = sem::Manager::Wrap(program->Types());
     builder.ast_ =
         builder.create<ast::Module>(program->AST().source, program->AST().GlobalDeclarations());
diff --git a/src/tint/program_builder.h b/src/tint/program_builder.h
index 9a4f7ca..c6b163f 100644
--- a/src/tint/program_builder.h
+++ b/src/tint/program_builder.h
@@ -299,6 +299,16 @@
     /// information
     bool IsValid() const;
 
+    /// @returns the last allocated (numerically highest) AST node identifier.
+    ast::NodeID LastAllocatedNodeID() const { return last_ast_node_id_; }
+
+    /// @returns the next sequentially unique node identifier.
+    ast::NodeID AllocateNodeID() {
+        auto out = ast::NodeID{last_ast_node_id_.value + 1};
+        last_ast_node_id_ = out;
+        return out;
+    }
+
     /// Creates a new ast::Node owned by the ProgramBuilder. When the
     /// ProgramBuilder is destructed, the ast::Node will also be destructed.
     /// @param source the Source of the node
@@ -307,7 +317,7 @@
     template <typename T, typename... ARGS>
     traits::EnableIfIsType<T, ast::Node>* create(const Source& source, ARGS&&... args) {
         AssertNotMoved();
-        return ast_nodes_.Create<T>(id_, source, std::forward<ARGS>(args)...);
+        return ast_nodes_.Create<T>(id_, AllocateNodeID(), source, std::forward<ARGS>(args)...);
     }
 
     /// Creates a new ast::Node owned by the ProgramBuilder, injecting the current
@@ -319,7 +329,7 @@
     template <typename T>
     traits::EnableIfIsType<T, ast::Node>* create() {
         AssertNotMoved();
-        return ast_nodes_.Create<T>(id_, source_);
+        return ast_nodes_.Create<T>(id_, AllocateNodeID(), source_);
     }
 
     /// Creates a new ast::Node owned by the ProgramBuilder, injecting the current
@@ -337,7 +347,7 @@
                      T>*
     create(ARG0&& arg0, ARGS&&... args) {
         AssertNotMoved();
-        return ast_nodes_.Create<T>(id_, source_, std::forward<ARG0>(arg0),
+        return ast_nodes_.Create<T>(id_, AllocateNodeID(), source_, std::forward<ARG0>(arg0),
                                     std::forward<ARGS>(args)...);
     }
 
@@ -2665,7 +2675,8 @@
     /// @param validation the validation to disable
     /// @returns the disable validation attribute pointer
     const ast::DisableValidationAttribute* Disable(ast::DisabledValidation validation) {
-        return ASTNodes().Create<ast::DisableValidationAttribute>(ID(), validation);
+        return ASTNodes().Create<ast::DisableValidationAttribute>(ID(), AllocateNodeID(),
+                                                                  validation);
     }
 
     /// Sets the current builder source to `src`
@@ -2774,6 +2785,7 @@
 
   private:
     ProgramID id_;
+    ast::NodeID last_ast_node_id_ = ast::NodeID{static_cast<decltype(ast::NodeID::value)>(0) - 1};
     sem::Manager types_;
     ASTNodeAllocator ast_nodes_;
     SemNodeAllocator sem_nodes_;
diff --git a/src/tint/reader/spirv/function.cc b/src/tint/reader/spirv/function.cc
index 2bcae95..7c10f89 100644
--- a/src/tint/reader/spirv/function.cc
+++ b/src/tint/reader/spirv/function.cc
@@ -5481,8 +5481,8 @@
 
         // Emit stub, will be removed by transform::SpirvAtomic
         auto sym = builder_.Symbols().New(std::string("stub_") + sem::str(builtin));
-        auto* stub_deco =
-            builder_.ASTNodes().Create<transform::SpirvAtomic::Stub>(builder_.ID(), builtin);
+        auto* stub_deco = builder_.ASTNodes().Create<transform::SpirvAtomic::Stub>(
+            builder_.ID(), builder_.AllocateNodeID(), builtin);
         auto* stub =
             create<ast::Function>(Source{}, sym, std::move(params), ret_type,
                                   /* body */ nullptr,
diff --git a/src/tint/reader/spirv/function.h b/src/tint/reader/spirv/function.h
index ae9ef31..ae7a9a1 100644
--- a/src/tint/reader/spirv/function.h
+++ b/src/tint/reader/spirv/function.h
@@ -376,7 +376,7 @@
 class StatementBuilder : public Castable<StatementBuilder, ast::Statement> {
   public:
     /// Constructor
-    StatementBuilder() : Base(ProgramID(), Source{}) {}
+    StatementBuilder() : Base(ProgramID(), ast::NodeID(), Source{}) {}
 
     /// @param builder the program builder
     /// @returns the build AST node
diff --git a/src/tint/reader/spirv/parser_impl.cc b/src/tint/reader/spirv/parser_impl.cc
index 11c439d..9803162 100644
--- a/src/tint/reader/spirv/parser_impl.cc
+++ b/src/tint/reader/spirv/parser_impl.cc
@@ -513,7 +513,8 @@
             return {
                 create<ast::StrideAttribute>(Source{}, decoration[1]),
                 builder_.ASTNodes().Create<ast::DisableValidationAttribute>(
-                    builder_.ID(), ast::DisabledValidation::kIgnoreStrideAttribute),
+                    builder_.ID(), builder_.AllocateNodeID(),
+                    ast::DisabledValidation::kIgnoreStrideAttribute),
             };
         }
         default:
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index cd28072..611f240 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -103,8 +103,7 @@
         return false;
     }
 
-    // Pre-allocate the ast -> sem map with the total number of AST nodes.
-    builder_->Sem().Reserve(builder_->ASTNodes().Count());
+    builder_->Sem().Reserve(builder_->LastAllocatedNodeID());
 
     if (!DependencyGraph::Build(builder_->AST(), builder_->Symbols(), builder_->Diagnostics(),
                                 dependencies_)) {
diff --git a/src/tint/resolver/validation_test.cc b/src/tint/resolver/validation_test.cc
index 20cf1ea..24f420b 100644
--- a/src/tint/resolver/validation_test.cc
+++ b/src/tint/resolver/validation_test.cc
@@ -50,13 +50,13 @@
 
 class FakeStmt final : public Castable<FakeStmt, ast::Statement> {
   public:
-    FakeStmt(ProgramID pid, Source src) : Base(pid, src) {}
+    FakeStmt(ProgramID pid, ast::NodeID nid, Source src) : Base(pid, nid, src) {}
     FakeStmt* Clone(CloneContext*) const override { return nullptr; }
 };
 
 class FakeExpr final : public Castable<FakeExpr, ast::Expression> {
   public:
-    FakeExpr(ProgramID pid, Source src) : Base(pid, src) {}
+    FakeExpr(ProgramID pid, ast::NodeID nid, Source src) : Base(pid, nid, src) {}
     FakeExpr* Clone(CloneContext*) const override { return nullptr; }
 };
 
diff --git a/src/tint/sem/info.h b/src/tint/sem/info.h
index aa98ff0..894b408 100644
--- a/src/tint/sem/info.h
+++ b/src/tint/sem/info.h
@@ -15,9 +15,12 @@
 #ifndef SRC_TINT_SEM_INFO_H_
 #define SRC_TINT_SEM_INFO_H_
 
+#include <algorithm>
 #include <type_traits>
 #include <unordered_map>
+#include <vector>
 
+#include "src/tint/ast/node.h"
 #include "src/tint/debug.h"
 #include "src/tint/sem/node.h"
 #include "src/tint/sem/type_mappings.h"
@@ -55,40 +58,42 @@
     /// @return this Program
     Info& operator=(Info&& rhs);
 
-    /// Pre-allocates the AST -> semantic node map to fit at least the given number of nodes.
-    /// @param num_ast_nodes the number of AST nodes to pre-allocate the map for.
-    void Reserve(size_t num_ast_nodes) { map_.reserve(num_ast_nodes); }
+    /// @param highest_node_id the last allocated (numerically highest) AST node identifier.
+    void Reserve(ast::NodeID highest_node_id) {
+        nodes_.resize(std::max(highest_node_id.value + 1, nodes_.size()));
+    }
 
-    /// Get looks up the semantic information for the AST node `node`.
+    /// Get looks up the semantic information for the AST node `ast_node`.
     /// @param ast_node the AST node
     /// @returns a pointer to the semantic node if found, otherwise nullptr
     template <typename SEM = InferFromAST,
               typename AST = CastableBase,
               typename RESULT = GetResultType<SEM, AST>>
     const RESULT* Get(const AST* ast_node) const {
-        auto it = map_.find(ast_node);
-        if (it == map_.end()) {
-            return nullptr;
+        if (ast_node && ast_node->node_id.value < nodes_.size()) {
+            return As<RESULT>(nodes_[ast_node->node_id.value]);
         }
-        return As<RESULT>(it->second);
+        return nullptr;
     }
 
-    /// Add registers the semantic node `sem_node` for the AST node `node`.
+    /// Add registers the semantic node `sem_node` for the AST node `ast_node`.
     /// @param ast_node the AST node
     /// @param sem_node the semantic node
     template <typename AST>
     void Add(const AST* ast_node, const SemanticNodeTypeFor<AST>* sem_node) {
-        // Check there's no semantic info already existing for the node
-        TINT_ASSERT(Semantic, map_.find(ast_node) == map_.end());
-        map_.emplace(ast_node, sem_node);
+        Reserve(ast_node->node_id);
+        // Check there's no semantic info already existing for the AST node
+        TINT_ASSERT(Semantic, nodes_[ast_node->node_id.value] == nullptr);
+        nodes_[ast_node->node_id.value] = sem_node;
     }
 
-    /// Replace replaces any existing semantic node `sem_node` for the AST node `node`.
+    /// Replace replaces any existing semantic node `sem_node` for the AST node `ast_node`.
     /// @param ast_node the AST node
     /// @param sem_node the new semantic node
     template <typename AST>
     void Replace(const AST* ast_node, const SemanticNodeTypeFor<AST>* sem_node) {
-        map_[ast_node] = sem_node;
+        Reserve(ast_node->node_id);
+        nodes_[ast_node->node_id.value] = sem_node;
     }
 
     /// Wrap returns a new Info created with the contents of `inner`.
@@ -100,7 +105,7 @@
     /// @return the Info that wraps `inner`
     static Info Wrap(const Info& inner) {
         Info out;
-        out.map_ = inner.map_;
+        out.nodes_ = inner.nodes_;
         out.module_ = inner.module_;
         return out;
     }
@@ -113,8 +118,8 @@
     const sem::Module* Module() const { return module_; }
 
   private:
-    // The map of AST node to semantic node
-    std::unordered_map<const ast::Node*, const sem::Node*> map_;
+    // AST node index to semantic node
+    std::vector<const sem::Node*> nodes_;
     // The semantic module
     sem::Module* module_ = nullptr;
 };
diff --git a/src/tint/symbol.cc b/src/tint/symbol.cc
index 0965697..925ab5b 100644
--- a/src/tint/symbol.cc
+++ b/src/tint/symbol.cc
@@ -23,7 +23,7 @@
 Symbol::Symbol(uint32_t val, tint::ProgramID program_id) : val_(val), program_id_(program_id) {}
 
 #if TINT_SYMBOL_STORE_DEBUG_NAME
-Symbol::Symbol(uint32_t val, tint::ProgramID program_id, std::string debug_name)
+Symbol::Symbol(uint32_t val, tint::ProgramID pid, NodeID nid, std::string debug_name)
     : val_(val), program_id_(program_id), debug_name_(std::move(debug_name)) {}
 #endif
 
diff --git a/src/tint/symbol.h b/src/tint/symbol.h
index 1cbc6b2..512deba 100644
--- a/src/tint/symbol.h
+++ b/src/tint/symbol.h
@@ -43,7 +43,7 @@
     /// @param val the symbol value
     /// @param program_id the identifier of the program that owns this Symbol
     /// @param debug_name name of symbols used only for debugging
-    Symbol(uint32_t val, tint::ProgramID program_id, std::string debug_name);
+    Symbol(uint32_t val, tint::ProgramID pid, NodeID nid, std::string debug_name);
 #endif
     /// Copy constructor
     /// @param o the symbol to copy
diff --git a/src/tint/transform/add_spirv_block_attribute.cc b/src/tint/transform/add_spirv_block_attribute.cc
index a62f9c7..85c20c3 100644
--- a/src/tint/transform/add_spirv_block_attribute.cc
+++ b/src/tint/transform/add_spirv_block_attribute.cc
@@ -70,7 +70,8 @@
             // This is a non-struct or a struct that is nested somewhere else, so we
             // need to wrap it first.
             auto* wrapper = utils::GetOrCreate(wrapper_structs, ty, [&]() {
-                auto* block = ctx.dst->ASTNodes().Create<SpirvBlockAttribute>(ctx.dst->ID());
+                auto* block = ctx.dst->ASTNodes().Create<SpirvBlockAttribute>(
+                    ctx.dst->ID(), ctx.dst->AllocateNodeID());
                 auto wrapper_name = ctx.src->Symbols().NameFor(var->symbol) + "_block";
                 auto* ret = ctx.dst->create<ast::Struct>(
                     ctx.dst->Symbols().New(wrapper_name),
@@ -89,7 +90,8 @@
             }
         } else {
             // Add a block attribute to this struct directly.
-            auto* block = ctx.dst->ASTNodes().Create<SpirvBlockAttribute>(ctx.dst->ID());
+            auto* block = ctx.dst->ASTNodes().Create<SpirvBlockAttribute>(
+                ctx.dst->ID(), ctx.dst->AllocateNodeID());
             ctx.InsertFront(str->Declaration()->attributes, block);
         }
     }
@@ -97,7 +99,8 @@
     ctx.Clone();
 }
 
-AddSpirvBlockAttribute::SpirvBlockAttribute::SpirvBlockAttribute(ProgramID pid) : Base(pid) {}
+AddSpirvBlockAttribute::SpirvBlockAttribute::SpirvBlockAttribute(ProgramID pid, ast::NodeID nid)
+    : Base(pid, nid) {}
 AddSpirvBlockAttribute::SpirvBlockAttribute::~SpirvBlockAttribute() = default;
 std::string AddSpirvBlockAttribute::SpirvBlockAttribute::InternalName() const {
     return "spirv_block";
@@ -105,7 +108,8 @@
 
 const AddSpirvBlockAttribute::SpirvBlockAttribute*
 AddSpirvBlockAttribute::SpirvBlockAttribute::Clone(CloneContext* ctx) const {
-    return ctx->dst->ASTNodes().Create<AddSpirvBlockAttribute::SpirvBlockAttribute>(ctx->dst->ID());
+    return ctx->dst->ASTNodes().Create<AddSpirvBlockAttribute::SpirvBlockAttribute>(
+        ctx->dst->ID(), ctx->dst->AllocateNodeID());
 }
 
 }  // namespace tint::transform
diff --git a/src/tint/transform/add_spirv_block_attribute.h b/src/tint/transform/add_spirv_block_attribute.h
index 67faaa5..51409c8 100644
--- a/src/tint/transform/add_spirv_block_attribute.h
+++ b/src/tint/transform/add_spirv_block_attribute.h
@@ -35,7 +35,8 @@
       public:
         /// Constructor
         /// @param program_id the identifier of the program that owns this node
-        explicit SpirvBlockAttribute(ProgramID program_id);
+        /// @param nid the unique node identifier
+        SpirvBlockAttribute(ProgramID program_id, ast::NodeID nid);
         /// Destructor
         ~SpirvBlockAttribute() override;
 
diff --git a/src/tint/transform/calculate_array_length.cc b/src/tint/transform/calculate_array_length.cc
index acf55a6..cfeaca8 100644
--- a/src/tint/transform/calculate_array_length.cc
+++ b/src/tint/transform/calculate_array_length.cc
@@ -57,7 +57,8 @@
 
 }  // namespace
 
-CalculateArrayLength::BufferSizeIntrinsic::BufferSizeIntrinsic(ProgramID pid) : Base(pid) {}
+CalculateArrayLength::BufferSizeIntrinsic::BufferSizeIntrinsic(ProgramID pid, ast::NodeID nid)
+    : Base(pid, nid) {}
 CalculateArrayLength::BufferSizeIntrinsic::~BufferSizeIntrinsic() = default;
 std::string CalculateArrayLength::BufferSizeIntrinsic::InternalName() const {
     return "intrinsic_buffer_size";
@@ -65,7 +66,8 @@
 
 const CalculateArrayLength::BufferSizeIntrinsic* CalculateArrayLength::BufferSizeIntrinsic::Clone(
     CloneContext* ctx) const {
-    return ctx->dst->ASTNodes().Create<CalculateArrayLength::BufferSizeIntrinsic>(ctx->dst->ID());
+    return ctx->dst->ASTNodes().Create<CalculateArrayLength::BufferSizeIntrinsic>(
+        ctx->dst->ID(), ctx->dst->AllocateNodeID());
 }
 
 CalculateArrayLength::CalculateArrayLength() = default;
@@ -109,7 +111,8 @@
                 },
                 ctx.dst->ty.void_(), nullptr,
                 ast::AttributeList{
-                    ctx.dst->ASTNodes().Create<BufferSizeIntrinsic>(ctx.dst->ID()),
+                    ctx.dst->ASTNodes().Create<BufferSizeIntrinsic>(ctx.dst->ID(),
+                                                                    ctx.dst->AllocateNodeID()),
                 },
                 ast::AttributeList{}));
 
diff --git a/src/tint/transform/calculate_array_length.h b/src/tint/transform/calculate_array_length.h
index 401e081..8db8dcc 100644
--- a/src/tint/transform/calculate_array_length.h
+++ b/src/tint/transform/calculate_array_length.h
@@ -40,7 +40,8 @@
       public:
         /// Constructor
         /// @param program_id the identifier of the program that owns this node
-        explicit BufferSizeIntrinsic(ProgramID program_id);
+        /// @param nid the unique node identifier
+        BufferSizeIntrinsic(ProgramID program_id, ast::NodeID nid);
         /// Destructor
         ~BufferSizeIntrinsic() override;
 
diff --git a/src/tint/transform/decompose_memory_access.cc b/src/tint/transform/decompose_memory_access.cc
index 48dae27..c3d5c8e 100644
--- a/src/tint/transform/decompose_memory_access.cc
+++ b/src/tint/transform/decompose_memory_access.cc
@@ -202,7 +202,8 @@
         return nullptr;
     }
     return builder->ASTNodes().Create<DecomposeMemoryAccess::Intrinsic>(
-        builder->ID(), DecomposeMemoryAccess::Intrinsic::Op::kLoad, storage_class, type);
+        builder->ID(), builder->AllocateNodeID(), DecomposeMemoryAccess::Intrinsic::Op::kLoad,
+        storage_class, type);
 }
 
 /// @returns a DecomposeMemoryAccess::Intrinsic attribute that can be applied
@@ -215,7 +216,8 @@
         return nullptr;
     }
     return builder->ASTNodes().Create<DecomposeMemoryAccess::Intrinsic>(
-        builder->ID(), DecomposeMemoryAccess::Intrinsic::Op::kStore, storage_class, type);
+        builder->ID(), builder->AllocateNodeID(), DecomposeMemoryAccess::Intrinsic::Op::kStore,
+        storage_class, type);
 }
 
 /// @returns a DecomposeMemoryAccess::Intrinsic attribute that can be applied
@@ -270,7 +272,7 @@
         return nullptr;
     }
     return builder->ASTNodes().Create<DecomposeMemoryAccess::Intrinsic>(
-        builder->ID(), op, ast::StorageClass::kStorage, type);
+        builder->ID(), builder->AllocateNodeID(), op, ast::StorageClass::kStorage, type);
 }
 
 /// BufferAccess describes a single storage or uniform buffer access
@@ -681,8 +683,12 @@
     }
 };
 
-DecomposeMemoryAccess::Intrinsic::Intrinsic(ProgramID pid, Op o, ast::StorageClass sc, DataType ty)
-    : Base(pid), op(o), storage_class(sc), type(ty) {}
+DecomposeMemoryAccess::Intrinsic::Intrinsic(ProgramID pid,
+                                            ast::NodeID nid,
+                                            Op o,
+                                            ast::StorageClass sc,
+                                            DataType ty)
+    : Base(pid, nid), op(o), storage_class(sc), type(ty) {}
 DecomposeMemoryAccess::Intrinsic::~Intrinsic() = default;
 std::string DecomposeMemoryAccess::Intrinsic::InternalName() const {
     std::stringstream ss;
@@ -771,8 +777,8 @@
 
 const DecomposeMemoryAccess::Intrinsic* DecomposeMemoryAccess::Intrinsic::Clone(
     CloneContext* ctx) const {
-    return ctx->dst->ASTNodes().Create<DecomposeMemoryAccess::Intrinsic>(ctx->dst->ID(), op,
-                                                                         storage_class, type);
+    return ctx->dst->ASTNodes().Create<DecomposeMemoryAccess::Intrinsic>(
+        ctx->dst->ID(), ctx->dst->AllocateNodeID(), op, storage_class, type);
 }
 
 bool DecomposeMemoryAccess::Intrinsic::IsAtomic() const {
diff --git a/src/tint/transform/decompose_memory_access.h b/src/tint/transform/decompose_memory_access.h
index 76cb23e..1d95dc4 100644
--- a/src/tint/transform/decompose_memory_access.h
+++ b/src/tint/transform/decompose_memory_access.h
@@ -72,11 +72,12 @@
         };
 
         /// Constructor
-        /// @param program_id the identifier of the program that owns this node
+        /// @param pid the identifier of the program that owns this node
+        /// @param nid the unique node identifier
         /// @param o the op of the intrinsic
         /// @param sc the storage class of the buffer
         /// @param ty the data type of the intrinsic
-        Intrinsic(ProgramID program_id, Op o, ast::StorageClass sc, DataType ty);
+        Intrinsic(ProgramID pid, ast::NodeID nid, Op o, ast::StorageClass sc, DataType ty);
         /// Destructor
         ~Intrinsic() override;
 
diff --git a/src/tint/transform/spirv_atomic.cc b/src/tint/transform/spirv_atomic.cc
index dc5b35c..0cdd4a3 100644
--- a/src/tint/transform/spirv_atomic.cc
+++ b/src/tint/transform/spirv_atomic.cc
@@ -273,14 +273,16 @@
 SpirvAtomic::SpirvAtomic() = default;
 SpirvAtomic::~SpirvAtomic() = default;
 
-SpirvAtomic::Stub::Stub(ProgramID pid, sem::BuiltinType b) : Base(pid), builtin(b) {}
+SpirvAtomic::Stub::Stub(ProgramID pid, ast::NodeID nid, sem::BuiltinType b)
+    : Base(pid, nid), builtin(b) {}
 SpirvAtomic::Stub::~Stub() = default;
 std::string SpirvAtomic::Stub::InternalName() const {
     return "@internal(spirv-atomic " + std::string(sem::str(builtin)) + ")";
 }
 
 const SpirvAtomic::Stub* SpirvAtomic::Stub::Clone(CloneContext* ctx) const {
-    return ctx->dst->ASTNodes().Create<SpirvAtomic::Stub>(ctx->dst->ID(), builtin);
+    return ctx->dst->ASTNodes().Create<SpirvAtomic::Stub>(ctx->dst->ID(),
+                                                          ctx->dst->AllocateNodeID(), builtin);
 }
 
 bool SpirvAtomic::ShouldRun(const Program* program, const DataMap&) const {
diff --git a/src/tint/transform/spirv_atomic.h b/src/tint/transform/spirv_atomic.h
index 36ac842..e1311c5 100644
--- a/src/tint/transform/spirv_atomic.h
+++ b/src/tint/transform/spirv_atomic.h
@@ -43,9 +43,10 @@
     /// translated to an atomic builtin.
     class Stub final : public Castable<Stub, ast::InternalAttribute> {
       public:
-        /// @param program_id the identifier of the program that owns this node
+        /// @param pid the identifier of the program that owns this node
+        /// @param nid the unique node identifier
         /// @param builtin the atomic builtin this stub represents
-        Stub(ProgramID program_id, sem::BuiltinType builtin);
+        Stub(ProgramID pid, ast::NodeID nid, sem::BuiltinType builtin);
         /// Destructor
         ~Stub() override;
 
diff --git a/src/tint/transform/spirv_atomic_test.cc b/src/tint/transform/spirv_atomic_test.cc
index 7f07d06..cbd8a26 100644
--- a/src/tint/transform/spirv_atomic_test.cc
+++ b/src/tint/transform/spirv_atomic_test.cc
@@ -49,14 +49,14 @@
                        b.Param("p1", b.ty.u32()),
                    },
                    b.ty.u32(), {b.Return(0_u)},
-                   {b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), a)});
+                   {b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), b.AllocateNodeID(), a)});
             b.Func(std::string{"stub_"} + sem::str(a) + "_i32",
                    {
                        b.Param("p0", b.ty.i32()),
                        b.Param("p1", b.ty.i32()),
                    },
                    b.ty.i32(), {b.Return(0_i)},
-                   {b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), a)});
+                   {b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), b.AllocateNodeID(), a)});
         }
 
         b.Func("stub_atomicLoad_u32",
@@ -65,7 +65,8 @@
                },
                b.ty.u32(), {b.Return(0_u)},
                {
-                   b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), sem::BuiltinType::kAtomicLoad),
+                   b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), b.AllocateNodeID(),
+                                                          sem::BuiltinType::kAtomicLoad),
                });
         b.Func("stub_atomicLoad_i32",
                {
@@ -73,7 +74,8 @@
                },
                b.ty.i32(), {b.Return(0_i)},
                {
-                   b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), sem::BuiltinType::kAtomicLoad),
+                   b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), b.AllocateNodeID(),
+                                                          sem::BuiltinType::kAtomicLoad),
                });
 
         b.Func("stub_atomicStore_u32",
@@ -83,7 +85,8 @@
                },
                b.ty.void_(), {},
                {
-                   b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), sem::BuiltinType::kAtomicStore),
+                   b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), b.AllocateNodeID(),
+                                                          sem::BuiltinType::kAtomicStore),
                });
         b.Func("stub_atomicStore_i32",
                {
@@ -92,7 +95,8 @@
                },
                b.ty.void_(), {},
                {
-                   b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), sem::BuiltinType::kAtomicStore),
+                   b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), b.AllocateNodeID(),
+                                                          sem::BuiltinType::kAtomicStore),
                });
 
         b.Func("stub_atomic_compare_exchange_weak_u32",
@@ -104,14 +108,14 @@
                b.ty.u32(), {b.Return(0_u)},
                {
                    b.ASTNodes().Create<SpirvAtomic::Stub>(
-                       b.ID(), sem::BuiltinType::kAtomicCompareExchangeWeak),
+                       b.ID(), b.AllocateNodeID(), sem::BuiltinType::kAtomicCompareExchangeWeak),
                });
         b.Func("stub_atomic_compare_exchange_weak_i32",
                {b.Param("p0", b.ty.i32()), b.Param("p1", b.ty.i32()), b.Param("p2", b.ty.i32())},
                b.ty.i32(), {b.Return(0_i)},
                {
                    b.ASTNodes().Create<SpirvAtomic::Stub>(
-                       b.ID(), sem::BuiltinType::kAtomicCompareExchangeWeak),
+                       b.ID(), b.AllocateNodeID(), sem::BuiltinType::kAtomicCompareExchangeWeak),
                });
 
         // Keep this pointer alive after Transform() returns
diff --git a/src/tint/writer/glsl/generator_impl.cc b/src/tint/writer/glsl/generator_impl.cc
index 88eec26..e9c0c4a 100644
--- a/src/tint/writer/glsl/generator_impl.cc
+++ b/src/tint/writer/glsl/generator_impl.cc
@@ -2210,7 +2210,7 @@
         }
 
         if (!Is<ast::ReturnStatement>(func->body->Last())) {
-            ast::ReturnStatement ret(ProgramID(), Source{});
+            ast::ReturnStatement ret(ProgramID{}, ast::NodeID{}, Source{});
             if (!EmitStatement(&ret)) {
                 return false;
             }
diff --git a/src/tint/writer/hlsl/generator_impl.cc b/src/tint/writer/hlsl/generator_impl.cc
index e8ab2c7..4516a98 100644
--- a/src/tint/writer/hlsl/generator_impl.cc
+++ b/src/tint/writer/hlsl/generator_impl.cc
@@ -3106,7 +3106,7 @@
         }
 
         if (!Is<ast::ReturnStatement>(func->body->Last())) {
-            ast::ReturnStatement ret(ProgramID(), Source{});
+            ast::ReturnStatement ret(ProgramID(), ast::NodeID{}, Source{});
             if (!EmitStatement(&ret)) {
                 return false;
             }
diff --git a/src/tint/writer/msl/generator_impl.cc b/src/tint/writer/msl/generator_impl.cc
index 2af36c9..68151e1 100644
--- a/src/tint/writer/msl/generator_impl.cc
+++ b/src/tint/writer/msl/generator_impl.cc
@@ -2039,7 +2039,7 @@
         }
 
         if (!Is<ast::ReturnStatement>(func->body->Last())) {
-            ast::ReturnStatement ret(ProgramID{}, Source{});
+            ast::ReturnStatement ret(ProgramID{}, ast::NodeID{}, Source{});
             if (!EmitStatement(&ret)) {
                 return false;
             }
diff --git a/src/tint/writer/spirv/builder.cc b/src/tint/writer/spirv/builder.cc
index 92fcf78..31ed539 100644
--- a/src/tint/writer/spirv/builder.cc
+++ b/src/tint/writer/spirv/builder.cc
@@ -778,22 +778,22 @@
         init_id = Switch(
             type,  //
             [&](const sem::F32*) {
-                ast::FloatLiteralExpression l(ProgramID{}, Source{}, 0,
+                ast::FloatLiteralExpression l(ProgramID{}, ast::NodeID{}, Source{}, 0,
                                               ast::FloatLiteralExpression::Suffix::kF);
                 return GenerateLiteralIfNeeded(override, &l);
             },
             [&](const sem::U32*) {
-                ast::IntLiteralExpression l(ProgramID{}, Source{}, 0,
+                ast::IntLiteralExpression l(ProgramID{}, ast::NodeID{}, Source{}, 0,
                                             ast::IntLiteralExpression::Suffix::kU);
                 return GenerateLiteralIfNeeded(override, &l);
             },
             [&](const sem::I32*) {
-                ast::IntLiteralExpression l(ProgramID{}, Source{}, 0,
+                ast::IntLiteralExpression l(ProgramID{}, ast::NodeID{}, Source{}, 0,
                                             ast::IntLiteralExpression::Suffix::kI);
                 return GenerateLiteralIfNeeded(override, &l);
             },
             [&](const sem::Bool*) {
-                ast::BoolLiteralExpression l(ProgramID{}, Source{}, false);
+                ast::BoolLiteralExpression l(ProgramID{}, ast::NodeID{}, Source{}, false);
                 return GenerateLiteralIfNeeded(override, &l);
             },
             [&](Default) {