resolver: Ensure that decorations aren't duplicated

Fixed: tint:525
Change-Id: I993b60f82ac5d5e07e1c76980604e6aaf1b94fb3
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/53806
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: James Price <jrprice@google.com>
diff --git a/src/ast/access_decoration.cc b/src/ast/access_decoration.cc
index 865422a..6a3b6f0 100644
--- a/src/ast/access_decoration.cc
+++ b/src/ast/access_decoration.cc
@@ -14,6 +14,8 @@
 
 #include "src/ast/access_decoration.h"
 
+#include <string>
+
 #include "src/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::AccessDecoration);
@@ -28,6 +30,10 @@
 
 AccessDecoration::~AccessDecoration() = default;
 
+std::string AccessDecoration::name() const {
+  return "access";
+}
+
 void AccessDecoration::to_str(const sem::Info&,
                               std::ostream& out,
                               size_t indent) const {
diff --git a/src/ast/access_decoration.h b/src/ast/access_decoration.h
index 2b385e9..cee1646 100644
--- a/src/ast/access_decoration.h
+++ b/src/ast/access_decoration.h
@@ -15,6 +15,8 @@
 #ifndef SRC_AST_ACCESS_DECORATION_H_
 #define SRC_AST_ACCESS_DECORATION_H_
 
+#include <string>
+
 #include "src/ast/access.h"
 #include "src/ast/decoration.h"
 
@@ -35,6 +37,9 @@
   /// @returns the access control value
   Access value() const { return value_; }
 
+  /// @returns the WGSL name for the decoration
+  std::string name() const override;
+
   /// Outputs the decoration to the given stream
   /// @param sem the semantic info for the program
   /// @param out the stream to write to
diff --git a/src/ast/binding_decoration.cc b/src/ast/binding_decoration.cc
index 4812635..f172eca 100644
--- a/src/ast/binding_decoration.cc
+++ b/src/ast/binding_decoration.cc
@@ -14,6 +14,8 @@
 
 #include "src/ast/binding_decoration.h"
 
+#include <string>
+
 #include "src/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::BindingDecoration);
@@ -28,6 +30,10 @@
 
 BindingDecoration::~BindingDecoration() = default;
 
+std::string BindingDecoration::name() const {
+  return "binding";
+}
+
 void BindingDecoration::to_str(const sem::Info&,
                                std::ostream& out,
                                size_t indent) const {
diff --git a/src/ast/binding_decoration.h b/src/ast/binding_decoration.h
index 627e3e7..fe4c3ee 100644
--- a/src/ast/binding_decoration.h
+++ b/src/ast/binding_decoration.h
@@ -15,6 +15,8 @@
 #ifndef SRC_AST_BINDING_DECORATION_H_
 #define SRC_AST_BINDING_DECORATION_H_
 
+#include <string>
+
 #include "src/ast/decoration.h"
 
 namespace tint {
@@ -33,6 +35,9 @@
   /// @returns the binding value
   uint32_t value() const { return value_; }
 
+  /// @returns the WGSL name for the decoration
+  std::string name() const override;
+
   /// Outputs the decoration to the given stream
   /// @param sem the semantic info for the program
   /// @param out the stream to write to
diff --git a/src/ast/builtin_decoration.cc b/src/ast/builtin_decoration.cc
index d4b0f83..58e93bf 100644
--- a/src/ast/builtin_decoration.cc
+++ b/src/ast/builtin_decoration.cc
@@ -14,6 +14,8 @@
 
 #include "src/ast/builtin_decoration.h"
 
+#include <string>
+
 #include "src/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::BuiltinDecoration);
@@ -28,6 +30,10 @@
 
 BuiltinDecoration::~BuiltinDecoration() = default;
 
+std::string BuiltinDecoration::name() const {
+  return "builtin";
+}
+
 void BuiltinDecoration::to_str(const sem::Info&,
                                std::ostream& out,
                                size_t indent) const {
diff --git a/src/ast/builtin_decoration.h b/src/ast/builtin_decoration.h
index 1ce952d..9711743 100644
--- a/src/ast/builtin_decoration.h
+++ b/src/ast/builtin_decoration.h
@@ -15,6 +15,8 @@
 #ifndef SRC_AST_BUILTIN_DECORATION_H_
 #define SRC_AST_BUILTIN_DECORATION_H_
 
+#include <string>
+
 #include "src/ast/builtin.h"
 #include "src/ast/decoration.h"
 
@@ -36,6 +38,9 @@
   /// @returns the builtin value
   Builtin value() const { return builtin_; }
 
+  /// @returns the WGSL name for the decoration
+  std::string name() const override;
+
   /// Outputs the decoration to the given stream
   /// @param sem the semantic info for the program
   /// @param out the stream to write to
diff --git a/src/ast/decoration.h b/src/ast/decoration.h
index 8618904..0b2912e 100644
--- a/src/ast/decoration.h
+++ b/src/ast/decoration.h
@@ -15,6 +15,7 @@
 #ifndef SRC_AST_DECORATION_H_
 #define SRC_AST_DECORATION_H_
 
+#include <string>
 #include <vector>
 
 #include "src/ast/node.h"
@@ -27,6 +28,9 @@
  public:
   ~Decoration() override;
 
+  /// @returns the WGSL name for the decoration
+  virtual std::string name() const = 0;
+
  protected:
   /// Constructor
   /// @param program_id the identifier of the program that owns this node
diff --git a/src/ast/disable_validation_decoration.cc b/src/ast/disable_validation_decoration.cc
index 59eb291..d71851d 100644
--- a/src/ast/disable_validation_decoration.cc
+++ b/src/ast/disable_validation_decoration.cc
@@ -28,7 +28,7 @@
 
 DisableValidationDecoration::~DisableValidationDecoration() = default;
 
-std::string DisableValidationDecoration::Name() const {
+std::string DisableValidationDecoration::InternalName() const {
   switch (validation_) {
     case DisabledValidation::kFunctionHasNoBody:
       return "disable_validation__function_has_no_body";
diff --git a/src/ast/disable_validation_decoration.h b/src/ast/disable_validation_decoration.h
index c94f166..8f6ecd2 100644
--- a/src/ast/disable_validation_decoration.h
+++ b/src/ast/disable_validation_decoration.h
@@ -56,7 +56,7 @@
 
   /// @return a short description of the internal decoration which will be
   /// displayed in WGSL as `[[internal(<name>)]]` (but is not parsable).
-  std::string Name() const override;
+  std::string InternalName() const override;
 
   /// Performs a deep clone of this object using the CloneContext `ctx`.
   /// @param ctx the clone context
diff --git a/src/ast/group_decoration.cc b/src/ast/group_decoration.cc
index 4e731c5..44175c7 100644
--- a/src/ast/group_decoration.cc
+++ b/src/ast/group_decoration.cc
@@ -14,6 +14,8 @@
 
 #include "src/ast/group_decoration.h"
 
+#include <string>
+
 #include "src/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::GroupDecoration);
@@ -28,6 +30,10 @@
 
 GroupDecoration::~GroupDecoration() = default;
 
+std::string GroupDecoration::name() const {
+  return "group";
+}
+
 void GroupDecoration::to_str(const sem::Info&,
                              std::ostream& out,
                              size_t indent) const {
diff --git a/src/ast/group_decoration.h b/src/ast/group_decoration.h
index 0e830d0..b621f1a 100644
--- a/src/ast/group_decoration.h
+++ b/src/ast/group_decoration.h
@@ -15,6 +15,8 @@
 #ifndef SRC_AST_GROUP_DECORATION_H_
 #define SRC_AST_GROUP_DECORATION_H_
 
+#include <string>
+
 #include "src/ast/decoration.h"
 
 namespace tint {
@@ -33,6 +35,9 @@
   /// @returns the group value
   uint32_t value() const { return value_; }
 
+  /// @returns the WGSL name for the decoration
+  std::string name() const override;
+
   /// Outputs the decoration to the given stream
   /// @param sem the semantic info for the program
   /// @param out the stream to write to
diff --git a/src/ast/internal_decoration.cc b/src/ast/internal_decoration.cc
index d915284..47db806 100644
--- a/src/ast/internal_decoration.cc
+++ b/src/ast/internal_decoration.cc
@@ -24,11 +24,15 @@
 
 InternalDecoration::~InternalDecoration() = default;
 
+std::string InternalDecoration::name() const {
+  return "internal";
+}
+
 void InternalDecoration::to_str(const sem::Info&,
                                 std::ostream& out,
                                 size_t indent) const {
   make_indent(out, indent);
-  out << "tint_internal(" << Name() << ")" << std::endl;
+  out << "tint_internal(" << InternalName() << ")" << std::endl;
 }
 
 }  // namespace ast
diff --git a/src/ast/internal_decoration.h b/src/ast/internal_decoration.h
index 94ef1ae..55bb65b 100644
--- a/src/ast/internal_decoration.h
+++ b/src/ast/internal_decoration.h
@@ -36,7 +36,10 @@
 
   /// @return a short description of the internal decoration which will be
   /// displayed in WGSL as `[[internal(<name>)]]` (but is not parsable).
-  virtual std::string Name() const = 0;
+  virtual std::string InternalName() const = 0;
+
+  /// @returns the WGSL name for the decoration
+  std::string name() const override;
 
   /// Writes a representation of the node to the output stream
   /// @param sem the semantic info for the program
diff --git a/src/ast/location_decoration.cc b/src/ast/location_decoration.cc
index dcf774f..932e6d1 100644
--- a/src/ast/location_decoration.cc
+++ b/src/ast/location_decoration.cc
@@ -14,6 +14,8 @@
 
 #include "src/ast/location_decoration.h"
 
+#include <string>
+
 #include "src/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::LocationDecoration);
@@ -28,6 +30,10 @@
 
 LocationDecoration::~LocationDecoration() = default;
 
+std::string LocationDecoration::name() const {
+  return "location";
+}
+
 void LocationDecoration::to_str(const sem::Info&,
                                 std::ostream& out,
                                 size_t indent) const {
diff --git a/src/ast/location_decoration.h b/src/ast/location_decoration.h
index 51c578d..415a330 100644
--- a/src/ast/location_decoration.h
+++ b/src/ast/location_decoration.h
@@ -15,6 +15,8 @@
 #ifndef SRC_AST_LOCATION_DECORATION_H_
 #define SRC_AST_LOCATION_DECORATION_H_
 
+#include <string>
+
 #include "src/ast/decoration.h"
 
 namespace tint {
@@ -35,6 +37,9 @@
   /// @returns the location value
   uint32_t value() const { return value_; }
 
+  /// @returns the WGSL name for the decoration
+  std::string name() const override;
+
   /// Outputs the decoration to the given stream
   /// @param sem the semantic info for the program
   /// @param out the stream to write to
diff --git a/src/ast/override_decoration.cc b/src/ast/override_decoration.cc
index bf32a48..942364b 100644
--- a/src/ast/override_decoration.cc
+++ b/src/ast/override_decoration.cc
@@ -14,6 +14,8 @@
 
 #include "src/ast/override_decoration.h"
 
+#include <string>
+
 #include "src/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::OverrideDecoration);
@@ -32,6 +34,10 @@
 
 OverrideDecoration::~OverrideDecoration() = default;
 
+std::string OverrideDecoration::name() const {
+  return "override";
+}
+
 void OverrideDecoration::to_str(const sem::Info&,
                                 std::ostream& out,
                                 size_t indent) const {
diff --git a/src/ast/override_decoration.h b/src/ast/override_decoration.h
index f5c6090..14ba825 100644
--- a/src/ast/override_decoration.h
+++ b/src/ast/override_decoration.h
@@ -15,6 +15,8 @@
 #ifndef SRC_AST_OVERRIDE_DECORATION_H_
 #define SRC_AST_OVERRIDE_DECORATION_H_
 
+#include <string>
+
 #include "src/ast/decoration.h"
 
 namespace tint {
@@ -40,6 +42,9 @@
   /// @returns the override id value
   uint32_t value() const { return value_; }
 
+  /// @returns the WGSL name for the decoration
+  std::string name() const override;
+
   /// Outputs the decoration to the given stream
   /// @param sem the semantic info for the program
   /// @param out the stream to write to
diff --git a/src/ast/stage_decoration.cc b/src/ast/stage_decoration.cc
index 74f3067..1843071 100644
--- a/src/ast/stage_decoration.cc
+++ b/src/ast/stage_decoration.cc
@@ -14,6 +14,8 @@
 
 #include "src/ast/stage_decoration.h"
 
+#include <string>
+
 #include "src/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::StageDecoration);
@@ -28,6 +30,10 @@
 
 StageDecoration::~StageDecoration() = default;
 
+std::string StageDecoration::name() const {
+  return "stage";
+}
+
 void StageDecoration::to_str(const sem::Info&,
                              std::ostream& out,
                              size_t indent) const {
diff --git a/src/ast/stage_decoration.h b/src/ast/stage_decoration.h
index f61403d..83ba14c 100644
--- a/src/ast/stage_decoration.h
+++ b/src/ast/stage_decoration.h
@@ -15,6 +15,8 @@
 #ifndef SRC_AST_STAGE_DECORATION_H_
 #define SRC_AST_STAGE_DECORATION_H_
 
+#include <string>
+
 #include "src/ast/decoration.h"
 #include "src/ast/pipeline_stage.h"
 
@@ -36,6 +38,9 @@
   /// @returns the stage
   PipelineStage value() const { return stage_; }
 
+  /// @returns the WGSL name for the decoration
+  std::string name() const override;
+
   /// Outputs the decoration to the given stream
   /// @param sem the semantic info for the program
   /// @param out the stream to write to
diff --git a/src/ast/stride_decoration.cc b/src/ast/stride_decoration.cc
index e343e6b..023984a 100644
--- a/src/ast/stride_decoration.cc
+++ b/src/ast/stride_decoration.cc
@@ -14,6 +14,8 @@
 
 #include "src/ast/stride_decoration.h"
 
+#include <string>
+
 #include "src/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::StrideDecoration);
@@ -28,6 +30,10 @@
 
 StrideDecoration::~StrideDecoration() = default;
 
+std::string StrideDecoration::name() const {
+  return "stride";
+}
+
 void StrideDecoration::to_str(const sem::Info&,
                               std::ostream& out,
                               size_t indent) const {
diff --git a/src/ast/stride_decoration.h b/src/ast/stride_decoration.h
index 05ebcab..e8102d9 100644
--- a/src/ast/stride_decoration.h
+++ b/src/ast/stride_decoration.h
@@ -15,6 +15,8 @@
 #ifndef SRC_AST_STRIDE_DECORATION_H_
 #define SRC_AST_STRIDE_DECORATION_H_
 
+#include <string>
+
 #include "src/ast/decoration.h"
 
 namespace tint {
@@ -33,6 +35,9 @@
   /// @returns the stride value
   uint32_t stride() const { return stride_; }
 
+  /// @returns the WGSL name for the decoration
+  std::string name() const override;
+
   /// Outputs the decoration to the given stream
   /// @param sem the semantic info for the program
   /// @param out the stream to write to
diff --git a/src/ast/struct_block_decoration.cc b/src/ast/struct_block_decoration.cc
index a18f6cf..75b9b9f 100644
--- a/src/ast/struct_block_decoration.cc
+++ b/src/ast/struct_block_decoration.cc
@@ -14,6 +14,8 @@
 
 #include "src/ast/struct_block_decoration.h"
 
+#include <string>
+
 #include "src/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::StructBlockDecoration);
@@ -27,6 +29,10 @@
 
 StructBlockDecoration::~StructBlockDecoration() = default;
 
+std::string StructBlockDecoration::name() const {
+  return "block";
+}
+
 void StructBlockDecoration::to_str(const sem::Info&,
                                    std::ostream& out,
                                    size_t indent) const {
diff --git a/src/ast/struct_block_decoration.h b/src/ast/struct_block_decoration.h
index 0d60912..9f77e37 100644
--- a/src/ast/struct_block_decoration.h
+++ b/src/ast/struct_block_decoration.h
@@ -15,6 +15,7 @@
 #ifndef SRC_AST_STRUCT_BLOCK_DECORATION_H_
 #define SRC_AST_STRUCT_BLOCK_DECORATION_H_
 
+#include <string>
 #include <vector>
 
 #include "src/ast/decoration.h"
@@ -32,6 +33,9 @@
   StructBlockDecoration(ProgramID program_id, const Source& source);
   ~StructBlockDecoration() override;
 
+  /// @returns the WGSL name for the decoration
+  std::string name() const override;
+
   /// Outputs the decoration to the given stream
   /// @param sem the semantic info for the program
   /// @param out the stream to write to
diff --git a/src/ast/struct_member_align_decoration.cc b/src/ast/struct_member_align_decoration.cc
index b4b78ea..d27ae3c 100644
--- a/src/ast/struct_member_align_decoration.cc
+++ b/src/ast/struct_member_align_decoration.cc
@@ -14,6 +14,8 @@
 
 #include "src/ast/struct_member_align_decoration.h"
 
+#include <string>
+
 #include "src/clone_context.h"
 #include "src/program_builder.h"
 
@@ -29,6 +31,10 @@
 
 StructMemberAlignDecoration::~StructMemberAlignDecoration() = default;
 
+std::string StructMemberAlignDecoration::name() const {
+  return "align";
+}
+
 void StructMemberAlignDecoration::to_str(const sem::Info&,
                                          std::ostream& out,
                                          size_t indent) const {
diff --git a/src/ast/struct_member_align_decoration.h b/src/ast/struct_member_align_decoration.h
index 49af979..b782e57 100644
--- a/src/ast/struct_member_align_decoration.h
+++ b/src/ast/struct_member_align_decoration.h
@@ -16,6 +16,7 @@
 #define SRC_AST_STRUCT_MEMBER_ALIGN_DECORATION_H_
 
 #include <stddef.h>
+#include <string>
 
 #include "src/ast/decoration.h"
 
@@ -38,6 +39,9 @@
   /// @returns the align value
   uint32_t align() const { return align_; }
 
+  /// @returns the WGSL name for the decoration
+  std::string name() const override;
+
   /// Outputs the decoration to the given stream
   /// @param sem the semantic info for the program
   /// @param out the stream to write to
diff --git a/src/ast/struct_member_offset_decoration.cc b/src/ast/struct_member_offset_decoration.cc
index 6e59d5e..4e03dda 100644
--- a/src/ast/struct_member_offset_decoration.cc
+++ b/src/ast/struct_member_offset_decoration.cc
@@ -14,6 +14,8 @@
 
 #include "src/ast/struct_member_offset_decoration.h"
 
+#include <string>
+
 #include "src/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::StructMemberOffsetDecoration);
@@ -28,6 +30,10 @@
 
 StructMemberOffsetDecoration::~StructMemberOffsetDecoration() = default;
 
+std::string StructMemberOffsetDecoration::name() const {
+  return "offset";
+}
+
 void StructMemberOffsetDecoration::to_str(const sem::Info&,
                                           std::ostream& out,
                                           size_t indent) const {
diff --git a/src/ast/struct_member_offset_decoration.h b/src/ast/struct_member_offset_decoration.h
index a7f880d..6bb4c09 100644
--- a/src/ast/struct_member_offset_decoration.h
+++ b/src/ast/struct_member_offset_decoration.h
@@ -15,6 +15,8 @@
 #ifndef SRC_AST_STRUCT_MEMBER_OFFSET_DECORATION_H_
 #define SRC_AST_STRUCT_MEMBER_OFFSET_DECORATION_H_
 
+#include <string>
+
 #include "src/ast/decoration.h"
 
 namespace tint {
@@ -45,6 +47,9 @@
   /// @returns the offset value
   uint32_t offset() const { return offset_; }
 
+  /// @returns the WGSL name for the decoration
+  std::string name() const override;
+
   /// Outputs the decoration to the given stream
   /// @param sem the semantic info for the program
   /// @param out the stream to write to
diff --git a/src/ast/struct_member_size_decoration.cc b/src/ast/struct_member_size_decoration.cc
index 4d9fb79..d8d283d 100644
--- a/src/ast/struct_member_size_decoration.cc
+++ b/src/ast/struct_member_size_decoration.cc
@@ -14,6 +14,8 @@
 
 #include "src/ast/struct_member_size_decoration.h"
 
+#include <string>
+
 #include "src/clone_context.h"
 #include "src/program_builder.h"
 
@@ -29,6 +31,10 @@
 
 StructMemberSizeDecoration::~StructMemberSizeDecoration() = default;
 
+std::string StructMemberSizeDecoration::name() const {
+  return "size";
+}
+
 void StructMemberSizeDecoration::to_str(const sem::Info&,
                                         std::ostream& out,
                                         size_t indent) const {
diff --git a/src/ast/struct_member_size_decoration.h b/src/ast/struct_member_size_decoration.h
index 8be17ce..0c8e120 100644
--- a/src/ast/struct_member_size_decoration.h
+++ b/src/ast/struct_member_size_decoration.h
@@ -16,6 +16,7 @@
 #define SRC_AST_STRUCT_MEMBER_SIZE_DECORATION_H_
 
 #include <stddef.h>
+#include <string>
 
 #include "src/ast/decoration.h"
 
@@ -38,6 +39,9 @@
   /// @returns the size value
   uint32_t size() const { return size_; }
 
+  /// @returns the WGSL name for the decoration
+  std::string name() const override;
+
   /// Outputs the decoration to the given stream
   /// @param sem the semantic info for the program
   /// @param out the stream to write to
diff --git a/src/ast/workgroup_decoration.cc b/src/ast/workgroup_decoration.cc
index 36a0d40..33e94a6 100644
--- a/src/ast/workgroup_decoration.cc
+++ b/src/ast/workgroup_decoration.cc
@@ -14,6 +14,8 @@
 
 #include "src/ast/workgroup_decoration.h"
 
+#include <string>
+
 #include "src/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::WorkgroupDecoration);
@@ -30,6 +32,10 @@
 
 WorkgroupDecoration::~WorkgroupDecoration() = default;
 
+std::string WorkgroupDecoration::name() const {
+  return "workgroup_size";
+}
+
 void WorkgroupDecoration::to_str(const sem::Info& sem,
                                  std::ostream& out,
                                  size_t indent) const {
diff --git a/src/ast/workgroup_decoration.h b/src/ast/workgroup_decoration.h
index fa68e87..ab6d029 100644
--- a/src/ast/workgroup_decoration.h
+++ b/src/ast/workgroup_decoration.h
@@ -16,6 +16,7 @@
 #define SRC_AST_WORKGROUP_DECORATION_H_
 
 #include <array>
+#include <string>
 
 #include "src/ast/decoration.h"
 
@@ -45,6 +46,9 @@
   /// @returns the workgroup dimensions
   std::array<ast::Expression*, 3> values() const { return {x_, y_, z_}; }
 
+  /// @returns the WGSL name for the decoration
+  std::string name() const override;
+
   /// Outputs the decoration to the given stream
   /// @param sem the semantic info for the program
   /// @param out the stream to write to
diff --git a/src/program_builder.h b/src/program_builder.h
index 5769545..02eae06 100644
--- a/src/program_builder.h
+++ b/src/program_builder.h
@@ -1909,6 +1909,22 @@
   }
 
   /// Creates an ast::WorkgroupDecoration
+  /// @param source the source information
+  /// @param x the x dimension expression
+  /// @param y the y dimension expression
+  /// @param z the z dimension expression
+  /// @returns the workgroup decoration pointer
+  template <typename EXPR_X, typename EXPR_Y, typename EXPR_Z>
+  ast::WorkgroupDecoration* WorkgroupSize(const Source& source,
+                                          EXPR_X&& x,
+                                          EXPR_Y&& y,
+                                          EXPR_Z&& z) {
+    return create<ast::WorkgroupDecoration>(
+        source, Expr(std::forward<EXPR_X>(x)), Expr(std::forward<EXPR_Y>(y)),
+        Expr(std::forward<EXPR_Z>(z)));
+  }
+
+  /// Creates an ast::WorkgroupDecoration
   /// @param x the x dimension expression
   /// @param y the y dimension expression
   /// @param z the z dimension expression
diff --git a/src/resolver/decoration_validation_test.cc b/src/resolver/decoration_validation_test.cc
index a5ca656..1296d30 100644
--- a/src/resolver/decoration_validation_test.cc
+++ b/src/resolver/decoration_validation_test.cc
@@ -159,6 +159,20 @@
                     TestParams{DecorationKind::kWorkgroup, false},
                     TestParams{DecorationKind::kBindingAndGroup, false}));
 
+TEST_F(FunctionReturnTypeDecorationTest, DuplicateDecoration) {
+  Func("main", ast::VariableList{}, ty.f32(), ast::StatementList{Return(1.f)},
+       ast::DecorationList{Stage(ast::PipelineStage::kCompute)},
+       ast::DecorationList{
+           Location(Source{{12, 34}}, 2),
+           Location(Source{{56, 78}}, 3),
+       });
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            R"(56:78 error: duplicate location decoration
+12:34 note: first decoration declared here)");
+}
+
 using ArrayDecorationTest = TestWithParams;
 TEST_P(ArrayDecorationTest, IsValid) {
   auto& params = GetParam();
@@ -232,6 +246,24 @@
                     TestParams{DecorationKind::kWorkgroup, false},
                     TestParams{DecorationKind::kBindingAndGroup, false}));
 
+TEST_F(StructDecorationTest, DuplicateDecoration) {
+  Structure("mystruct",
+            {
+                Member("a", ty.i32()),
+            },
+            {
+                create<ast::StructBlockDecoration>(Source{{12, 34}}),
+                create<ast::StructBlockDecoration>(Source{{56, 78}}),
+            });
+
+  WrapInFunction();
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            R"(56:78 error: duplicate block decoration
+12:34 note: first decoration declared here)");
+}
+
 using StructMemberDecorationTest = TestWithParams;
 TEST_P(StructMemberDecorationTest, IsValid) {
   auto& params = GetParam();
@@ -268,6 +300,25 @@
                     TestParams{DecorationKind::kWorkgroup, false},
                     TestParams{DecorationKind::kBindingAndGroup, false}));
 
+TEST_F(StructMemberDecorationTest, DuplicateDecoration) {
+  Structure("mystruct", {
+                            Member("a", ty.i32(),
+                                   {
+                                       create<ast::StructMemberAlignDecoration>(
+                                           Source{{12, 34}}, 4u),
+                                       create<ast::StructMemberAlignDecoration>(
+                                           Source{{56, 78}}, 8u),
+                                   }),
+                        });
+
+  WrapInFunction();
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            R"(56:78 error: duplicate align decoration
+12:34 note: first decoration declared here)");
+}
+
 using VariableDecorationTest = TestWithParams;
 TEST_P(VariableDecorationTest, IsValid) {
   auto& params = GetParam();
@@ -310,6 +361,22 @@
                     TestParams{DecorationKind::kWorkgroup, false},
                     TestParams{DecorationKind::kBindingAndGroup, true}));
 
+TEST_F(VariableDecorationTest, DuplicateDecoration) {
+  Global("a", ty.sampler(ast::SamplerKind::kSampler),
+         ast::DecorationList{
+             create<ast::BindingDecoration>(Source{{12, 34}}, 2),
+             create<ast::GroupDecoration>(2),
+             create<ast::BindingDecoration>(Source{{56, 78}}, 3),
+         });
+
+  WrapInFunction();
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            R"(56:78 error: duplicate binding decoration
+12:34 note: first decoration declared here)");
+}
+
 using ConstantDecorationTest = TestWithParams;
 TEST_P(ConstantDecorationTest, IsValid) {
   auto& params = GetParam();
@@ -344,6 +411,21 @@
                     TestParams{DecorationKind::kWorkgroup, false},
                     TestParams{DecorationKind::kBindingAndGroup, false}));
 
+TEST_F(ConstantDecorationTest, DuplicateDecoration) {
+  GlobalConst("a", ty.f32(), Expr(1.23f),
+              ast::DecorationList{
+                  create<ast::OverrideDecoration>(Source{{12, 34}}),
+                  create<ast::OverrideDecoration>(Source{{56, 78}}, 1),
+              });
+
+  WrapInFunction();
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            R"(56:78 error: duplicate override decoration
+12:34 note: first decoration declared here)");
+}
+
 using FunctionDecorationTest = TestWithParams;
 TEST_P(FunctionDecorationTest, IsValid) {
   auto& params = GetParam();
@@ -485,18 +567,19 @@
         ParamsFor<mat3x3<f32>>((default_mat3x3.align - 1) * 7, false),
         ParamsFor<mat4x4<f32>>((default_mat4x4.align - 1) * 7, false)));
 
-TEST_F(ArrayStrideTest, MultipleDecorations) {
+TEST_F(ArrayStrideTest, DuplicateDecoration) {
   auto* arr = ty.array(Source{{12, 34}}, ty.i32(), 4,
                        {
-                           create<ast::StrideDecoration>(4),
-                           create<ast::StrideDecoration>(4),
+                           create<ast::StrideDecoration>(Source{{12, 34}}, 4),
+                           create<ast::StrideDecoration>(Source{{56, 78}}, 4),
                        });
 
   Global("myarray", arr, ast::StorageClass::kInput);
 
   EXPECT_FALSE(r()->Resolve());
   EXPECT_EQ(r()->error(),
-            "12:34 error: array must have at most one [[stride]] decoration");
+            R"(56:78 error: duplicate stride decoration
+12:34 note: first decoration declared here)");
 }
 
 }  // namespace
@@ -700,15 +783,18 @@
             "compute stages");
 }
 
-TEST_F(WorkgroupDecoration, MultipleAttributes) {
+TEST_F(WorkgroupDecoration, DuplicateDecoration) {
   Func(Source{{12, 34}}, "main", {}, ty.void_(), {},
-       {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1),
-        WorkgroupSize(2)});
+       {
+           Stage(ast::PipelineStage::kCompute),
+           WorkgroupSize(Source{{12, 34}}, 1, nullptr, nullptr),
+           WorkgroupSize(Source{{56, 78}}, 2, nullptr, nullptr),
+       });
 
   EXPECT_FALSE(r()->Resolve());
   EXPECT_EQ(r()->error(),
-            "12:34 error: only one workgroup_size attribute permitted per "
-            "entry point");
+            R"(56:78 error: duplicate workgroup_size decoration
+12:34 note: first decoration declared here)");
 }
 
 }  // namespace
diff --git a/src/resolver/function_validation_test.cc b/src/resolver/function_validation_test.cc
index cc72217..11b704c 100644
--- a/src/resolver/function_validation_test.cc
+++ b/src/resolver/function_validation_test.cc
@@ -267,14 +267,14 @@
            Return(),
        },
        ast::DecorationList{
-           Stage(ast::PipelineStage::kVertex),
-           Stage(ast::PipelineStage::kFragment),
+           Stage(Source{{12, 34}}, ast::PipelineStage::kVertex),
+           Stage(Source{{56, 78}}, ast::PipelineStage::kFragment),
        });
 
   EXPECT_FALSE(r()->Resolve());
   EXPECT_EQ(r()->error(),
-            "12:34 error v-0020: only one stage decoration permitted per entry "
-            "point");
+            R"(56:78 error: duplicate stage decoration
+12:34 note: first decoration declared here)");
 }
 
 TEST_F(ResolverFunctionValidationTest, NoPipelineEntryPoints) {
diff --git a/src/resolver/resolver.cc b/src/resolver/resolver.cc
index fa34926..27fbd1b 100644
--- a/src/resolver/resolver.cc
+++ b/src/resolver/resolver.cc
@@ -573,6 +573,10 @@
     }
   }
 
+  if (!ValidateNoDuplicateDecorations(var->decorations())) {
+    return false;
+  }
+
   if (auto bp = var->binding_point()) {
     info->binding_point = {bp.group->value(), bp.binding->value()};
   }
@@ -608,6 +612,10 @@
     return false;
   }
 
+  if (!ValidateNoDuplicateDecorations(info->declaration->decorations())) {
+    return false;
+  }
+
   for (auto* deco : info->declaration->decorations()) {
     if (info->declaration->is_const()) {
       if (auto* override_deco = deco->As<ast::OverrideDecoration>()) {
@@ -872,18 +880,6 @@
       return false;
     }
   }
-  if (stage_deco_count > 1) {
-    diagnostics_.add_error(
-        "v-0020", "only one stage decoration permitted per entry point",
-        func->source());
-    return false;
-  }
-  if (workgroup_deco_count > 1) {
-    diagnostics_.add_error(
-        "only one workgroup_size attribute permitted per entry point",
-        func->source());
-    return false;
-  }
 
   for (auto* param : func->params()) {
     if (!ValidateParameter(variable_to_info_.at(param))) {
@@ -1197,6 +1193,9 @@
     for (auto* deco : param->decorations()) {
       Mark(deco);
     }
+    if (!ValidateNoDuplicateDecorations(param->decorations())) {
+      return false;
+    }
 
     variable_stack_.set(param->symbol(), param_info);
     info->parameters.emplace_back(param_info);
@@ -1282,9 +1281,16 @@
   for (auto* deco : func->decorations()) {
     Mark(deco);
   }
+  if (!ValidateNoDuplicateDecorations(func->decorations())) {
+    return false;
+  }
+
   for (auto* deco : func->return_type_decorations()) {
     Mark(deco);
   }
+  if (!ValidateNoDuplicateDecorations(func->return_type_decorations())) {
+    return false;
+  }
 
   // Set work-group size defaults.
   for (int i = 0; i < 3; i++) {
@@ -2777,16 +2783,15 @@
     return nullptr;
   }
 
+  if (!ValidateNoDuplicateDecorations(arr->decorations())) {
+    return nullptr;
+  }
+
   // Look for explicit stride via [[stride(n)]] decoration
   uint32_t explicit_stride = 0;
   for (auto* deco : arr->decorations()) {
     Mark(deco);
     if (auto* sd = deco->As<ast::StrideDecoration>()) {
-      if (explicit_stride) {
-        diagnostics_.add_error(
-            "array must have at most one [[stride]] decoration", source);
-        return nullptr;
-      }
       explicit_stride = sd->stride();
       if (!ValidateArrayStrideDecoration(sd, el_size, el_align, source)) {
         return nullptr;
@@ -2916,6 +2921,9 @@
 }
 
 sem::Struct* Resolver::Structure(const ast::Struct* str) {
+  if (!ValidateNoDuplicateDecorations(str->decorations())) {
+    return nullptr;
+  }
   for (auto* deco : str->decorations()) {
     Mark(deco);
   }
@@ -2961,6 +2969,10 @@
       return nullptr;
     }
 
+    if (!ValidateNoDuplicateDecorations(member->decorations())) {
+      return nullptr;
+    }
+
     bool has_offset_deco = false;
     bool has_align_deco = false;
     bool has_size_deco = false;
@@ -3201,6 +3213,22 @@
   return true;
 }
 
+bool Resolver::ValidateNoDuplicateDecorations(
+    const ast::DecorationList& decorations) {
+  std::unordered_map<const TypeInfo*, Source> seen;
+  for (auto* d : decorations) {
+    auto res = seen.emplace(&d->TypeInfo(), d->source());
+    if (!res.second) {
+      diagnostics_.add_error("duplicate " + d->name() + " decoration",
+                             d->source());
+      diagnostics_.add_note("first decoration declared here",
+                            res.first->second);
+      return false;
+    }
+  }
+  return true;
+}
+
 bool Resolver::ApplyStorageClassUsageToType(ast::StorageClass sc,
                                             sem::Type* ty,
                                             const Source& usage) {
diff --git a/src/resolver/resolver.h b/src/resolver/resolver.h
index de33472..03715fd 100644
--- a/src/resolver/resolver.h
+++ b/src/resolver/resolver.h
@@ -271,6 +271,7 @@
   bool ValidateVectorConstructor(const ast::TypeConstructorExpression* ctor,
                                  const sem::Vector* vec_type);
   bool ValidateTypeDecl(const ast::TypeDecl* named_type) const;
+  bool ValidateNoDuplicateDecorations(const ast::DecorationList& decorations);
 
   /// @returns the sem::Type for the ast::Type `ty`, building it if it
   /// hasn't been constructed already. If an error is raised, nullptr is
diff --git a/src/transform/calculate_array_length.cc b/src/transform/calculate_array_length.cc
index 19490df..0feadc1 100644
--- a/src/transform/calculate_array_length.cc
+++ b/src/transform/calculate_array_length.cc
@@ -56,7 +56,7 @@
     ProgramID program_id)
     : Base(program_id) {}
 CalculateArrayLength::BufferSizeIntrinsic::~BufferSizeIntrinsic() = default;
-std::string CalculateArrayLength::BufferSizeIntrinsic::Name() const {
+std::string CalculateArrayLength::BufferSizeIntrinsic::InternalName() const {
   return "intrinsic_buffer_size";
 }
 
diff --git a/src/transform/calculate_array_length.h b/src/transform/calculate_array_length.h
index b4d8b0b..ba0279a 100644
--- a/src/transform/calculate_array_length.h
+++ b/src/transform/calculate_array_length.h
@@ -43,7 +43,7 @@
     ~BufferSizeIntrinsic() override;
 
     /// @return "buffer_size"
-    std::string Name() const override;
+    std::string InternalName() const override;
 
     /// Performs a deep clone of this object using the CloneContext `ctx`.
     /// @param ctx the clone context
diff --git a/src/transform/decompose_storage_access.cc b/src/transform/decompose_storage_access.cc
index b42b303..29077b3 100644
--- a/src/transform/decompose_storage_access.cc
+++ b/src/transform/decompose_storage_access.cc
@@ -564,7 +564,7 @@
 DecomposeStorageAccess::Intrinsic::Intrinsic(ProgramID program_id, Type ty)
     : Base(program_id), type(ty) {}
 DecomposeStorageAccess::Intrinsic::~Intrinsic() = default;
-std::string DecomposeStorageAccess::Intrinsic::Name() const {
+std::string DecomposeStorageAccess::Intrinsic::InternalName() const {
   switch (type) {
     case kLoadU32:
       return "intrinsic_load_u32";
diff --git a/src/transform/decompose_storage_access.h b/src/transform/decompose_storage_access.h
index 3da2c1e..ad593b9 100644
--- a/src/transform/decompose_storage_access.h
+++ b/src/transform/decompose_storage_access.h
@@ -74,7 +74,7 @@
 
     /// @return a short description of the internal decoration which will be
     /// displayed as `[[internal(<name>)]]`
-    std::string Name() const override;
+    std::string InternalName() const override;
 
     /// Performs a deep clone of this object using the CloneContext `ctx`.
     /// @param ctx the clone context
diff --git a/src/writer/wgsl/generator_impl.cc b/src/writer/wgsl/generator_impl.cc
index 8dc1c01..5e706b6 100644
--- a/src/writer/wgsl/generator_impl.cc
+++ b/src/writer/wgsl/generator_impl.cc
@@ -667,7 +667,7 @@
     } else if (auto* align = deco->As<ast::StructMemberAlignDecoration>()) {
       out_ << "align(" << align->align() << ")";
     } else if (auto* internal = deco->As<ast::InternalDecoration>()) {
-      out_ << "internal(" << internal->Name() << ")";
+      out_ << "internal(" << internal->InternalName() << ")";
     } else {
       TINT_ICE(diagnostics_)
           << "Unsupported decoration '" << deco->TypeInfo().name << "'";