[wgsl] Add num_subgroups builtin

This is just the WGSL frontend support. The builtin is not hooked up
in any backend yet.

Bug: 454652043
Change-Id: Ia5ad1e1a5e37185ae5195201d9b45a1473210c62
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/268576
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Commit-Queue: James Price <jrprice@google.com>
diff --git a/src/tint/cmd/bench/enums_core_bench.cc b/src/tint/cmd/bench/enums_core_bench.cc
index c0532be..cbf34b7 100644
--- a/src/tint/cmd/bench/enums_core_bench.cc
+++ b/src/tint/cmd/bench/enums_core_bench.cc
@@ -1074,76 +1074,83 @@
         "ltcal_invocation_index",
         "local_in1ocation_index",
         "localAAinvocation_inDDex",
-        "Om_woP2grups",
-        "num_workgoups",
-        "num_workgro77ps",
+        "Pum_2uboOps",
+        "numsubgroups",
+        "num_77ubgroups",
+        "num_subgroups",
+        "num_smmUbAAro1s",
+        "nAAm_Xubgroufs",
+        "nuv_VVuvgrous",
+        "num_workgrppups",
+        "numTTworkgroups",
+        "num_workgjjok0ps",
         "num_workgroups",
-        "n1m_UUorkAAroups",
-        "Xum_workgrfupAA",
-        "VVum_wrkgrouvs",
-        "posippion",
-        "posTTtion",
-        "kosjj0tion",
+        "um_workgroups",
+        "nu_yyorkgroups",
+        "nuzz_wokgrouYYs",
+        "positWion",
+        "0o7tiUUn",
+        "UUosipi",
         "position",
-        "posiion",
-        "posiyyon",
-        "poYizzion",
-        "priWitiiie_index",
-        "6rimitv7_indUUx",
-        "prUUitive_inpe",
+        "hTstion",
+        "posi5on",
+        "itio1",
+        "priIitimme_i44dex",
+        "priYRYYDie_index",
+        "primjjtive_index",
         "primitive_index",
-        "hrimitiveiTTex",
-        "primitie_inde5",
-        "primtveind1x",
-        "sam44mme_iIdex",
-        "saYDpRe_indYY",
-        "sampjje_index",
+        "prmitive_index",
+        "QQrimtide_index",
+        "primtive_in5Hex",
+        "saxpkFF_index",
+        "sapreindex",
+        "sampleKKi_Nex",
         "sample_index",
-        "sample_idex",
-        "smple_iQQdex",
-        "55amHle_idex",
-        "saxFple_mask",
-        "sapD_mrsk",
-        "sNmple__KKsk",
+        "sample_aTdex",
+        "samplevvindex",
+        "amplejjindex",
+        "samplnnOOmas",
+        "sggOOpleOmsk",
+        "sawwQpe_mask",
         "sample_mask",
-        "saxalT_mask",
-        "sample_vvask",
-        "sjjmple_mak",
-        "subgrnnOOp_i",
-        "sggOOgroOpid",
-        "suwwQgoup_id",
+        "sampl_masJ",
+        "sample_masD",
+        "OOample_msk",
+        "subgroup_d",
+        "subro0p_id",
+        "lbgroup_d",
         "subgroup_id",
-        "subgrup_iJ",
-        "subgroup_iD",
-        "OOubgroupid",
-        "subgroup_nvocation_id",
-        "subro00p_invocation_i",
-        "subgroup_vqcation_ld",
+        "subgroup_iu",
+        "su33group_id",
+        "subgrouNNid",
+        "subgroup_inocaton_Md",
+        "subg22oup_Rnocationbbid",
+        "subgroup_ivocatiiin_id",
         "subgroup_invocation_id",
-        "subgroup_invocation_iu",
-        "subgroup_invo33ation_id",
-        "subgrouNNinvocation_id",
-        "subrMup_sze",
-        "ubgr22up_sRzbb",
-        "subgiiup_size",
+        "ubgroup_invocation_id",
+        "subgup_inCoocution_id",
+        "subHHoup_inUoaJion_id",
+        "subghrou_yize",
+        "WWugroup_BBze",
+        "EEbccroup_sCEze",
         "subgroup_size",
-        "subgrup_size",
-        "soobgop_sCue",
-        "subgrHHu_izJ",
-        "vrtyg_ihdggx",
-        "vrex_WWndeBB",
-        "vertex_inECEcc",
+        "subgreep_siGyy",
+        "uUUroup44size",
+        "subgroup_sie",
+        "verteN_index",
+        "MMertexssindex",
+        "vertexDindCx",
         "vertex_index",
-        "veGtex_ieedyy",
-        "ve44teUindex",
-        "vertex_inde",
-        "workgNoup_id",
-        "MMorkgrssup_id",
-        "workgrDup_Cd",
+        "ggTYtex_index",
+        "vertjjx_index",
+        "vrtDDx_inde",
+        "99MMkEEroup_id",
+        "wNrkgrou_id",
+        "55orkgroup_id",
         "workgroup_id",
-        "ggTYkgroup_id",
-        "workjjroup_id",
-        "wrkDDroup_i",
+        "Qorkgroup_id",
+        "workgronnp_i",
+        "workFFroWWid",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
@@ -1156,41 +1163,41 @@
 BENCHMARK(BuiltinValueParser);
 void InterpolationSamplingParser(::benchmark::State& state) {
     const char* kStrings[] = {
-        "cEEMM9er",
-        "cNter",
-        "55enter",
+        "ce66tew",
+        "cvFter",
+        "enWWr",
         "center",
-        "Qenter",
-        "nnnter",
-        "cenWFr",
-        "centro66w",
-        "cvntroFd",
-        "cWWntrd",
+        "cntr",
+        "5tK",
+        "cen4er",
+        "cnPtrggd",
+        "UentrSS",
+        "centro2V",
         "centroid",
-        "entrod",
-        "5Ktri",
-        "c4ntroid",
-        "gtnnPr",
-        "eSShe",
-        "2itVer",
+        "cpntroid",
+        "X9VVn9r2id",
+        "ntrnd",
+        "ser",
+        "8BBte",
+        "AyACCher",
         "either",
-        "epther",
-        "eiXV9992",
-        "nte",
-        "Bs",
-        "fiBB",
-        "yCCAAAt",
+        "eihBv",
+        "A4QQter",
+        "ZZtheT4",
+        "Xirlt",
+        "fiddt",
+        "fis",
         "first",
-        "ivit",
-        "A4rsQQ",
-        "T4Zist",
-        "Xamlle",
-        "ddamle",
-        "sadd",
+        "dis1",
+        "GGvs",
+        "jjst",
+        "samwle",
+        "Xamnle",
+        "ja9ple",
         "sample",
-        "si1le",
-        "vmpGG",
-        "jjmpl",
+        "sape",
+        "sam2WWe",
+        "samle",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
@@ -1203,27 +1210,27 @@
 BENCHMARK(InterpolationSamplingParser);
 void InterpolationTypeParser(::benchmark::State& state) {
     const char* kStrings[] = {
-        "fwat",
-        "flXn",
-        "fljt",
+        "flV",
+        "fnzal",
+        "Ylt",
         "flat",
-        "fz",
-        "flWW2",
-        "fat",
-        "lneVr",
-        "lnz4ear",
-        "li__Y",
+        "HMkat",
+        "fzzDI",
+        "dla",
+        "lineh",
+        "lFMMlaN",
+        "lina",
         "linear",
-        "linHkaM",
-        "lInzzaD",
-        "qnedr",
-        "persehtive",
-        "perlMMNctiFe",
-        "perspetiv",
+        "9Anx",
+        "VVnnar",
+        "iKer",
+        "perBBCecive",
+        "perpoctive",
+        "perspeccwtEE8e",
         "perspective",
-        "xersA9tiv",
-        "VVnrspetive",
-        "perspecKv",
+        "erspectivbb",
+        "6erspective",
+        "erspectgg",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
@@ -1236,286 +1243,286 @@
 BENCHMARK(InterpolationTypeParser);
 void TexelFormatParser(::benchmark::State& state) {
     const char* kStrings[] = {
-        "bgra8nCBBm",
-        "bgrauoorm",
-        "bgra8ccnE88rw",
+        "braIun11Em",
+        "bgra8unrm",
+        "pHgra8uorm",
         "bgra8unorm",
-        "bgr8bbnorm",
-        "b6ra8unorm",
-        "gKaggnrm",
-        "r16fE1at",
-        "r16floa",
-        "rHpfloat",
+        "bgqqaa8uGorm",
+        "bgra8unm",
+        "bgra8unrxxxx",
+        "r1zzfloaSS",
+        "r16foat",
+        "1o4oat",
         "r16float",
-        "qqaa6flGat",
-        "r6loat",
-        "xxxx6_loat",
-        "r16SSzznt",
-        "r16int",
-        "16ont",
+        "r16fla",
+        "rKflat",
+        "rzz6flct",
+        "1gg88ant",
+        "r1jjsint",
+        "riA6sin1",
         "r16sint",
-        "r16nt",
-        "rK6st",
-        "rczsint",
-        "r88an5ggm",
-        "r16snsjrm",
-        "116snoriA",
+        "rlVVs66nt",
+        "6sic",
+        "r16GGnht",
+        "jiiyysnor",
+        "rv_QQsnttm",
+        "r6soLLm",
         "r16snorm",
-        "rVV6sl66rm",
-        "6norm",
-        "tt1Ghmnom",
-        "iiyy6int",
-        "QQt1_vuin",
-        "rLuint",
+        "r16Wno7m",
+        "6svvoFr",
+        "ryysnzzrm",
+        "16uio",
+        "r16uI_c",
+        "rdd6int",
         "r16uint",
-        "r1uWcct",
-        "r16uFt",
-        "rzz6yyin",
-        "runorm",
-        "I16Rco_m",
-        "r16uddrm",
+        "lM6ui2",
+        "r1Ouint",
+        "rKK6muiTT",
+        "rRR6no",
+        "4uno00",
+        "r16nqor",
         "r16unorm",
-        "M1l22or",
-        "O16unorm",
-        "KK6TTunmrm",
-        "rRR2lo",
-        "4flo00",
-        "r32nqoa",
+        "r16unorBB",
+        "r16u2WBarm",
+        "r16ujjorm",
+        "rt6HHlot",
+        "3JJflat",
+        "r3ooSlttat",
         "r32float",
-        "r32floaBB",
-        "r32f2WBaat",
-        "r32fjjoat",
-        "66tinHH",
-        "r3JJst",
-        "rttoosiSt",
-        "r32sint",
-        "2r_SSnt",
-        "rWW2sint",
-        "r32sink",
+        "rSS2far_",
+        "r32flWWat",
+        "r32flokt",
         "r3bbitt",
-        "r32u1n",
-        "rf2uiN",
-        "r32uint",
+        "r32s1n",
+        "rf2siN",
+        "r32sint",
         "rii2iVt",
         "r32winU",
-        "r3Funt",
-        "448s8LLnt",
-        "r8sinZZ",
-        "rMMsint",
+        "r3Fsnt",
+        "r38LLuin44",
+        "r32uiZZt",
+        "MM32uint",
+        "r32uint",
+        "r3uint",
+        "rSSMMu6At",
+        "GGgxuinTT",
+        "rIsint",
+        "l8sint",
+        "r8OOn",
         "r8sint",
-        "8sint",
-        "SSAM6int",
-        "GGxTTint",
-        "r8snorI",
-        "r8slorm",
-        "rOOsnm",
+        "r8s5n",
+        "r8zRint",
+        "r8siPLL",
+        "r8snhhm",
+        "rsnorm",
+        "8snjm",
         "r8snorm",
-        "8aKo5m",
-        "Rf__zzrm",
-        "r8LLnorP",
-        "ruihht",
-        "ruint",
-        "ruit",
+        "r8BB0",
+        "xxL8nrm",
+        "rIIsnorMM",
+        "rXFFig",
+        "rzssunp",
+        "rZZrrinF",
         "r8uint",
-        "08BB",
-        "xxL8un",
-        "r8uiMMIt",
-        "r8XnFFm",
-        "r8unsszz",
-        "r8rrnZZFm",
+        "r8jir00",
+        "rwwuirr",
+        "r8iZQ",
+        "rn__nrh",
+        "r8worm",
+        "r3unorm",
         "r8unorm",
-        "rrrjor00",
-        "p8wrrorm",
-        "QZuorm",
-        "rg__hb1nfloat",
-        "rg11w10ufloa",
-        "rg11b103float",
+        "runoiXm",
+        "r83m",
+        "rOvv8orm",
+        "rg1Pbvju5loat",
+        "rgi1b1Nfloat",
+        "rg21b10ufloa",
         "rg11b10ufloat",
-        "rg11b0XXfloai",
-        "rg31b10loa",
-        "rg11c10u8vvoOO",
-        "rg16vlPaj",
-        "ugi6plot",
-        "g16f2oat",
+        "rg11b1ufDJqat",
+        "r11b10mQloat",
+        "rg11bJ0uflat",
+        "rg16fWWat",
+        "wQg16gglwat",
+        "rg1zfloat",
         "rg16float",
-        "rgD6fJqat",
-        "rg1Qfmoa",
-        "rg16fJat",
-        "rg1sWWnt",
-        "rQQ1wswgnt",
-        "rg16sinz",
+        "rWW6flzzat",
+        "rg6flo",
+        "rKUU6fla99q",
+        "RRgEE6snt",
+        "r1sint",
+        "rg16sttt",
         "rg16sint",
-        "rgWWsinzz",
-        "rg1it",
-        "rK996qqUUn",
-        "EEg16sRRom",
-        "rg16nor",
-        "rg1tsnorm",
+        "rg1MMin",
+        "raaKKTBBint",
+        "l5516sIIn",
+        "rg16sooBBw",
+        "PPg1__snorm",
+        "rg16RRnorm",
         "rg16snorm",
-        "rgqnoMMm",
-        "rgT6aanBBrKK",
-        "rl16sII5rm",
-        "rg16wBBoo",
-        "rg1__PPint",
-        "rRR16uint",
+        "rghSom",
+        "Q1srm",
+        "r6sn8HHrm",
+        "rg1YYuDnt",
+        "rg1Ouint",
+        "Jg8IIuinZ",
         "rg16uint",
-        "rgSut",
-        "riQt",
-        "g6uHH8nt",
-        "rD16unYYrm",
-        "rg16unoOm",
-        "r8II6ZJorm",
+        "ogY6uint",
+        "r0X6uint",
+        "rg6KKSncn",
+        "GGg1633nr88",
+        "rXX133unoqqm",
+        "rz16no4m",
         "rg16unorm",
-        "rgoYunorm",
-        "rg6unor0X",
-        "rcKn6unoSm",
-        "GGg3233la88",
-        "rXX333floqqt",
-        "rz32lo4t",
+        "rWttWKnhhrm",
+        "rg1rxorm",
+        "rg16CQerm",
+        "g3rYloat",
+        "rg32xmloat",
+        "r3float",
         "rg32float",
-        "rWttWKlhhat",
-        "rg3rxoat",
-        "rg32CQeat",
-        "rg32sYr",
-        "xxgm2sinS",
-        "rg3rsit",
+        "rg3244ylyyat",
+        "r3wwfloat",
+        "r32fBBaii",
+        "rgdd2ssint",
+        "rgMsinee",
+        "Tg32sint",
         "rg32sint",
-        "rgyy2sit",
-        "r82i8t",
-        "rge2sint",
-        "hkkg3uint",
-        "rg2uOOBnt",
-        "g32ui9t",
+        "r82s00l",
+        "6mmg1Lsint",
+        "Gg32IYYnt",
+        "rYY2uLVVt",
+        "m62s32uinI",
+        "r32uinjj",
         "rg32uint",
-        "rgg2uDnt",
-        "rgna2in1",
-        "6gIuinS",
-        "rgsinYY",
-        "Lg8YVVin",
-        "rg8ms2I66",
+        "ppg2u7",
+        "Ng32ui6t",
+        "rg32uinsgg",
+        "g8sint",
+        "JgOOsqnt",
+        "rg00sinMM",
         "rg8sint",
-        "jjg8snt",
-        "pp7nt",
-        "rgN6int",
-        "rg8snorsgg",
-        "rgsnorm",
-        "rg8sJoOOm",
+        "rMM8smmnt",
+        "rH8sqqt",
+        "gGsin88",
+        "gRRswworm",
+        "rgqsnorm",
+        "gUsom",
         "rg8snorm",
-        "MMg8snor00",
-        "rg8snmmMMm",
-        "r8snqHrm",
-        "gGuin88",
-        "wwgRRuit",
-        "rg8uinq",
+        "g8sBo5MQ",
+        "33g8snorm",
+        "Og8sno33m",
+        "w8unt",
+        "rgAkkM_t",
+        "rg8int",
         "rg8uint",
-        "8uiU",
-        "BQ85nMM",
-        "rg8u33nt",
-        "Og8uno33m",
-        "rg8uom",
-        "kkgAunb_M",
+        "dg8i",
+        "rXXI55wwnt",
+        "g8uint",
+        "g8dorm",
+        "dJJunom",
+        "rg8_orm",
         "rg8unorm",
-        "rg8unom",
-        "rduom",
-        "rg55wXXnorm",
-        "rb10a2uint",
-        "rgb10a2ut",
-        "rgb1Jadint",
+        "rgnorL",
+        "99g8unrm",
+        "xU8unorm",
+        "9gb10a2uit",
+        "rvb10a2uint",
+        "LLgb10a22uint",
         "rgb10a2uint",
-        "rb_1a2uint",
-        "rgLL0aint",
-        "rg10G2uin99",
-        "xxgb102UUnom",
-        "rgb90a2unom",
-        "rgb10a2uvorm",
+        "rg1bf1uint",
+        "rgb1AAa2uin",
+        "CU10a2ui",
+        "rgb1zza2unwnm",
+        "MMwVVb10a2unonm",
+        "rgb10aNNXorm",
         "rgb10a2unorm",
-        "rgb10LL22unorm",
-        "rgb11a2ufom",
-        "rgb0aAAungrm",
-        "CUa16flo",
-        "rgbw1zzflont",
-        "rgna16flowVVMM",
+        "rgb10a2un77Wee",
+        "vvgb133a2PPRorm",
+        "rg2210a2unorjj",
+        "rgbaAA6flot",
+        "kkba1NfRRoat",
+        "rQQba16float",
         "rgba16float",
-        "rgNNaX6flat",
-        "rgbee77flWWat",
-        "rgb3316PPloavv",
-        "22gba16sjjnt",
-        "cgAA16sint",
-        "Ngbakk6RRin",
+        "rgb16fuuoat",
+        "uuba16flat",
+        "rga1floaZ",
+        "rgHa16snt",
+        "rgdSSS16sint",
+        "rgba16sTTnt",
         "rgba16sint",
-        "rgba16sinQQ",
-        "rgbuu6sint",
-        "rba6suunt",
-        "rga1snorZ",
-        "rgba1sHorm",
-        "rgba1dsnoSSSm",
+        "rmb16ctnt",
+        "rgba16shhnt",
+        "rgba16siG",
+        "HHgbaww6snAArbb",
+        "rgba1BBsnorm",
+        "rgbM2hsnogm",
         "rgba16snorm",
-        "rgba16snTTrm",
-        "rba16cmort",
-        "rgba16snohhm",
-        "rgba16uiG",
-        "rHbbwAAa16uint",
-        "rgba16uBBnt",
+        "rgba1snorWW",
+        "rgbqq16snorm",
+        "rgbar6snUUm",
+        "rgba6uiEt",
+        "rrVVba16uRnt",
+        "rgbH31GGOint",
         "rgba16uint",
-        "hga226Mint",
-        "rgba16unWW",
-        "rqqba16uint",
-        "rgbar6unUUm",
-        "rgba16Eorm",
-        "rgRa16unoVVrr",
+        "rgba1ui55",
+        "rgBa16uiSSt",
+        "rga16uazz",
+        "rga16noAA66",
+        "rgbw6_norm",
+        "rgba6unorm",
         "rgba16unorm",
-        "rg33OGG1Hunom",
-        "55g16Dnorm",
-        "rSSbB16unoQm",
-        "zzgba32lat",
-        "rga32loAA66",
-        "rgbw2_loat",
+        "rgbr16norm",
+        "rgba16unRRrG",
+        "KK1ba1LLunoxm",
+        "rgba32NWWlFat",
+        "rgba3GfoUt",
+        "r6bEEFF2flat",
         "rgba32float",
-        "rgba2float",
-        "rgbr32loat",
-        "rgba32flRRaG",
-        "KxUbLL31sint",
-        "rgba32NNWWFt",
-        "gbfGUsint",
-        "rgba32sint",
-        "rgba3FFE6nt",
-        "rgba39yyJTTnt",
-        "zgba32sit",
-        "rb32uinI",
+        "99TJJbyy3float",
+        "rzb32float",
+        "gba3Iloat",
         "gb32pt",
-        "ttgnn32uint",
+        "ttgnn32sint",
+        "rgba3isiKt",
+        "rgba32sint",
+        "reeba32sint",
+        "rgbg_2si11t",
+        "rgb32sint",
+        "rgba3ont",
+        "rpbX3ui",
+        "rgYY6ss2uin",
         "rgba32uint",
-        "rgba3iuiKt",
-        "reeba32uint",
-        "rgbg_2ui11t",
-        "rgba8snt",
-        "rgbaino",
-        "rpX8si",
+        "r77ba3YuInt",
+        "rgbaSSeeuinG",
+        "rgbQluint",
+        "gba8int",
+        "rgba__sint",
+        "rDDba8sin",
         "rgba8sint",
-        "g6ss8sYYnt",
-        "rIIba8s7Yt",
-        "ngbGSSeeint",
-        "rgbQlnorm",
-        "ba8snorm",
-        "rgba8sn__rm",
-        "rgba8snorm",
-        "rgba8soDDm",
-        "rgb8snrm",
-        "rgb5l55nor1",
+        "rga8int",
+        "551balsint",
         "rgbc4int",
-        "gba8uint",
-        "gba6Ou11nt",
-        "rgba8uint",
-        "rsa8unt",
-        "rgbaXuint",
-        "rgbw844int",
+        "gba8snorm",
+        "rgOa811no6m",
+        "rgasnosm",
+        "rgba8snorm",
+        "rgba8snXrm",
+        "rg44w8snorm",
         "rgba8YYnorm",
-        "gba8unorm",
-        "rTa8unyrm",
+        "rgba8int",
+        "gTTa8ynt",
+        "rga8vvbbt",
+        "rgba8uint",
+        "rgbakuint",
+        "g99a5uit",
+        "lRRbauinjj",
+        "YYgb8u77orm",
+        "rbaLLJrm",
+        "rgnnunorm",
         "rgba8unorm",
-        "bbba8unovv",
-        "rgba8unokm",
-        "Vb58unor9",
+        "rooba8uord",
+        "rba8Enobm",
+        "rba8unorm",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
diff --git a/src/tint/cmd/fuzz/wgsl/dictionary.txt b/src/tint/cmd/fuzz/wgsl/dictionary.txt
index 8940160..d450174 100644
--- a/src/tint/cmd/fuzz/wgsl/dictionary.txt
+++ b/src/tint/cmd/fuzz/wgsl/dictionary.txt
@@ -320,6 +320,7 @@
 "must_use"
 "normalize"
 "num_levels"
+"num_subgroups"
 "num_workgroups"
 "off"
 "original_value"
diff --git a/src/tint/lang/core/core.def b/src/tint/lang/core/core.def
index 49076dd..14c414e 100644
--- a/src/tint/lang/core/core.def
+++ b/src/tint/lang/core/core.def
@@ -65,6 +65,7 @@
   subgroup_id
   subgroup_invocation_id
   subgroup_size
+  num_subgroups
   barycentric_coord
   primitive_index
   @internal __point_size
diff --git a/src/tint/lang/core/enums.cc b/src/tint/lang/core/enums.cc
index fe2f146..3d3e8a0 100644
--- a/src/tint/lang/core/enums.cc
+++ b/src/tint/lang/core/enums.cc
@@ -997,6 +997,9 @@
     if (str == "local_invocation_index") {
         return BuiltinValue::kLocalInvocationIndex;
     }
+    if (str == "num_subgroups") {
+        return BuiltinValue::kNumSubgroups;
+    }
     if (str == "num_workgroups") {
         return BuiltinValue::kNumWorkgroups;
     }
@@ -1053,6 +1056,8 @@
             return "local_invocation_id";
         case BuiltinValue::kLocalInvocationIndex:
             return "local_invocation_index";
+        case BuiltinValue::kNumSubgroups:
+            return "num_subgroups";
         case BuiltinValue::kNumWorkgroups:
             return "num_workgroups";
         case BuiltinValue::kPosition:
diff --git a/src/tint/lang/core/enums.h b/src/tint/lang/core/enums.h
index 181cb7b..3f38a8c 100644
--- a/src/tint/lang/core/enums.h
+++ b/src/tint/lang/core/enums.h
@@ -583,6 +583,7 @@
     kInstanceIndex,
     kLocalInvocationId,
     kLocalInvocationIndex,
+    kNumSubgroups,
     kNumWorkgroups,
     kPosition,
     kPrimitiveIndex,
@@ -622,6 +623,7 @@
     "instance_index",
     "local_invocation_id",
     "local_invocation_index",
+    "num_subgroups",
     "num_workgroups",
     "position",
     "primitive_index",
diff --git a/src/tint/lang/core/enums_test.cc b/src/tint/lang/core/enums_test.cc
index 03eb456..df6ee7a 100644
--- a/src/tint/lang/core/enums_test.cc
+++ b/src/tint/lang/core/enums_test.cc
@@ -959,6 +959,7 @@
     {"instance_index", BuiltinValue::kInstanceIndex},
     {"local_invocation_id", BuiltinValue::kLocalInvocationId},
     {"local_invocation_index", BuiltinValue::kLocalInvocationIndex},
+    {"num_subgroups", BuiltinValue::kNumSubgroups},
     {"num_workgroups", BuiltinValue::kNumWorkgroups},
     {"position", BuiltinValue::kPosition},
     {"primitive_index", BuiltinValue::kPrimitiveIndex},
@@ -996,36 +997,39 @@
     {"locaZZ_invocation_index", BuiltinValue::kUndefined},
     {"local_2nvocationOinIIPUx", BuiltinValue::kUndefined},
     {"local_invocatiZZn_index", BuiltinValue::kUndefined},
-    {"num_workgroupnn", BuiltinValue::kUndefined},
-    {"nu22_ZZHrkgkkoups", BuiltinValue::kUndefined},
-    {"num_workgrops", BuiltinValue::kUndefined},
-    {"osiiRn", BuiltinValue::kUndefined},
-    {"posqqt99on", BuiltinValue::kUndefined},
-    {"posit77on", BuiltinValue::kUndefined},
-    {"prmitive3inex", BuiltinValue::kUndefined},
-    {"primitiveindeccu", BuiltinValue::kUndefined},
-    {"priRRitive_i1ex", BuiltinValue::kUndefined},
-    {"sllple_inJJex", BuiltinValue::kUndefined},
-    {"sImplMM_ix", BuiltinValue::kUndefined},
-    {"s66mpleinTex", BuiltinValue::kUndefined},
-    {"sQQmpleJmask", BuiltinValue::kUndefined},
-    {"suumpemask", BuiltinValue::kUndefined},
-    {"sampl_mak", BuiltinValue::kUndefined},
-    {"ygqoup_i33", BuiltinValue::kUndefined},
-    {"subgrouxx_id", BuiltinValue::kUndefined},
-    {"subgrrN_d", BuiltinValue::kUndefined},
-    {"su99group_nvocation_id", BuiltinValue::kUndefined},
-    {"subgoup_invocation_id", BuiltinValue::kUndefined},
-    {"subgloup_inHocaton_id", BuiltinValue::kUndefined},
-    {"sug_oup_sie", BuiltinValue::kUndefined},
-    {"subgroup_sze", BuiltinValue::kUndefined},
-    {"jbgroup_size", BuiltinValue::kUndefined},
-    {"EEetttmmx_index", BuiltinValue::kUndefined},
-    {"verte_ndex", BuiltinValue::kUndefined},
-    {"vertex_irrdx", BuiltinValue::kUndefined},
-    {"workgrxupid", BuiltinValue::kUndefined},
-    {"zzorkgou_id", BuiltinValue::kUndefined},
-    {"workgroup_ed", BuiltinValue::kUndefined},
+    {"num_subgroupnn", BuiltinValue::kUndefined},
+    {"kkZ22m_subgrHups", BuiltinValue::kUndefined},
+    {"num_subgrous", BuiltinValue::kUndefined},
+    {"nu_workrouRs", BuiltinValue::kUndefined},
+    {"num_w99rkqqroups", BuiltinValue::kUndefined},
+    {"num_workgro77ps", BuiltinValue::kUndefined},
+    {"posi3o", BuiltinValue::kUndefined},
+    {"posiuuccn", BuiltinValue::kUndefined},
+    {"osiRR1on", BuiltinValue::kUndefined},
+    {"primitive_indeJJ", BuiltinValue::kUndefined},
+    {"rimitie_MMIex", BuiltinValue::kUndefined},
+    {"p66Tmitve7index", BuiltinValue::kUndefined},
+    {"QQampJe_index", BuiltinValue::kUndefined},
+    {"sAuue_index", BuiltinValue::kUndefined},
+    {"ampe_index", BuiltinValue::kUndefined},
+    {"ypqe_mas33", BuiltinValue::kUndefined},
+    {"sample_xxask", BuiltinValue::kUndefined},
+    {"samprrNak", BuiltinValue::kUndefined},
+    {"su99roup_id", BuiltinValue::kUndefined},
+    {"subgoup_id", BuiltinValue::kUndefined},
+    {"slbgrup_Hd", BuiltinValue::kUndefined},
+    {"subgrp_invocation__d", BuiltinValue::kUndefined},
+    {"subgroup_invocaion_id", BuiltinValue::kUndefined},
+    {"sujgroup_nvocation_id", BuiltinValue::kUndefined},
+    {"suttgrommp_sizEE", BuiltinValue::kUndefined},
+    {"ubgoup_size", BuiltinValue::kUndefined},
+    {"subgrourr_siz", BuiltinValue::kUndefined},
+    {"vertexxinex", BuiltinValue::kUndefined},
+    {"zzerte_idex", BuiltinValue::kUndefined},
+    {"vertex_ndex", BuiltinValue::kUndefined},
+    {"worppgroup_id", BuiltinValue::kUndefined},
+    {"wWrTTgrup_iuu", BuiltinValue::kUndefined},
+    {"workgrut0_iTT", BuiltinValue::kUndefined},
 };
 
 using BuiltinValueParseTest = testing::TestWithParam<BuiltinValueCase>;
@@ -1084,60 +1088,60 @@
 };
 
 static constexpr AttributeCase kInvalidAttributeCases[] = {
-    {"uigZp", Attribute::kUndefined},
-    {"00ui7TT", Attribute::kUndefined},
-    {"vvJJ", Attribute::kUndefined},
-    {"biQding", Attribute::kUndefined},
-    {"bCdRng", Attribute::kUndefined},
+    {"vvn", Attribute::kUndefined},
+    {"aliQn", Attribute::kUndefined},
+    {"al3R", Attribute::kUndefined},
     {"iCi", Attribute::kUndefined},
-    {"blnPPp_srr", Attribute::kUndefined},
-    {"xx8DDuen_src", Attribute::kUndefined},
+    {"bidiPPpr", Attribute::kUndefined},
+    {"buDDndixxg", Attribute::kUndefined},
     {"lldqqendYYsrc", Attribute::kUndefined},
-    {"uiFFti__", Attribute::kUndefined},
-    {"rrGGNNtin", Attribute::kUndefined},
+    {"bFF__nd_sr", Attribute::kUndefined},
+    {"rrleNNGd_sr", Attribute::kUndefined},
     {"buiMlin", Attribute::kUndefined},
-    {"lo", Attribute::kUndefined},
-    {"xllor", Attribute::kUndefined},
+    {"utin", Attribute::kUndefined},
+    {"blilqix", Attribute::kUndefined},
     {"olor", Attribute::kUndefined},
-    {"camp44e", Attribute::kUndefined},
-    {"WWGmpute", Attribute::kUndefined},
+    {"ao44r", Attribute::kUndefined},
+    {"coloWW", Attribute::kUndefined},
     {"cjjmpue", Attribute::kUndefined},
-    {"djjagnostic", Attribute::kUndefined},
-    {"diagnoj1c", Attribute::kUndefined},
+    {"cojjpute", Attribute::kUndefined},
+    {"co1jte", Attribute::kUndefined},
     {"diagnotic", Attribute::kUndefined},
-    {"99ragment", Attribute::kUndefined},
-    {"fVVagmeny", Attribute::kUndefined},
+    {"diag99ostic", Attribute::kUndefined},
+    {"dyagnosVVic", Attribute::kUndefined},
     {"frxmeZZt", Attribute::kUndefined},
-    {"g33vvp", Attribute::kUndefined},
-    {"gros9", Attribute::kUndefined},
+    {"fravvme33", Attribute::kUndefined},
+    {"fsa9ment", Attribute::kUndefined},
     {"grFu", Attribute::kUndefined},
-    {"ue", Attribute::kUndefined},
-    {"Z", Attribute::kUndefined},
-    {"i", Attribute::kUndefined},
-    {"inpMt_attFchmnt_index", Attribute::kUndefined},
-    {"inputWWatZZachment66index", Attribute::kUndefined},
+    {"ueoup", Attribute::kUndefined},
+    {"rIKK", Attribute::kUndefined},
+    {"J", Attribute::kUndefined},
+    {"CSSXd", Attribute::kUndefined},
+    {"JWWZZ", Attribute::kUndefined},
     {"inpt_attacme5t_indmdx", Attribute::kUndefined},
-    {"BBnterpBlaUe", Attribute::kUndefined},
-    {"inter0olatJ11", Attribute::kUndefined},
+    {"BBnput_atacBBmeUt_index", Attribute::kUndefined},
+    {"input0attaJJhme11t_index", Attribute::kUndefined},
     {"intfrpottate", Attribute::kUndefined},
-    {"inXaittn", Attribute::kUndefined},
-    {"inLwriant", Attribute::kUndefined},
+    {"intrttolte", Attribute::kUndefined},
+    {"interpLlwte", Attribute::kUndefined},
     {"in1ariant", Attribute::kUndefined},
-    {"lowwation", Attribute::kUndefined},
-    {"latien", Attribute::kUndefined},
+    {"invarianww", Attribute::kUndefined},
+    {"ineeiat", Attribute::kUndefined},
     {"loction", Attribute::kUndefined},
-    {"muNNt_use", Attribute::kUndefined},
-    {"mustUlRRs__", Attribute::kUndefined},
+    {"loNNation", Attribute::kUndefined},
+    {"locaUlRRo__", Attribute::kUndefined},
     {"mHst_use", Attribute::kUndefined},
-    {"srCCe", Attribute::kUndefined},
-    {"ize", Attribute::kUndefined},
+    {"muCCr_use", Attribute::kUndefined},
+    {"ust_use", Attribute::kUndefined},
     {"dzIp", Attribute::kUndefined},
-    {"vetex", Attribute::kUndefined},
-    {"LNtx", Attribute::kUndefined},
+    {"ize", Attribute::kUndefined},
+    {"LN", Attribute::kUndefined},
     {"r", Attribute::kUndefined},
-    {"wxxrkgqqoup_GizRR", Attribute::kUndefined},
-    {"workSroup_siGGe", Attribute::kUndefined},
+    {"vxxGteqqR", Attribute::kUndefined},
+    {"GGerteS", Attribute::kUndefined},
     {"oqkccr8up_size", Attribute::kUndefined},
+    {"workgroup_sze", Attribute::kUndefined},
+    {"woppkgroup_sie", Attribute::kUndefined},
 };
 
 using AttributeParseTest = testing::TestWithParam<AttributeCase>;
diff --git a/src/tint/lang/core/ir/binary/decode.cc b/src/tint/lang/core/ir/binary/decode.cc
index 7d7e118..7de9940 100644
--- a/src/tint/lang/core/ir/binary/decode.cc
+++ b/src/tint/lang/core/ir/binary/decode.cc
@@ -1714,6 +1714,8 @@
                 return core::BuiltinValue::kSubgroupInvocationId;
             case pb::BuiltinValue::subgroup_size:
                 return core::BuiltinValue::kSubgroupSize;
+            case pb::BuiltinValue::num_subgroups:
+                return core::BuiltinValue::kNumSubgroups;
             case pb::BuiltinValue::vertex_index:
                 return core::BuiltinValue::kVertexIndex;
             case pb::BuiltinValue::workgroup_id:
diff --git a/src/tint/lang/core/ir/binary/encode.cc b/src/tint/lang/core/ir/binary/encode.cc
index 6f6c2e9..88bdbde 100644
--- a/src/tint/lang/core/ir/binary/encode.cc
+++ b/src/tint/lang/core/ir/binary/encode.cc
@@ -1080,6 +1080,8 @@
                 return pb::BuiltinValue::subgroup_invocation_id;
             case core::BuiltinValue::kSubgroupSize:
                 return pb::BuiltinValue::subgroup_size;
+            case core::BuiltinValue::kNumSubgroups:
+                return pb::BuiltinValue::num_subgroups;
             case core::BuiltinValue::kVertexIndex:
                 return pb::BuiltinValue::vertex_index;
             case core::BuiltinValue::kWorkgroupId:
diff --git a/src/tint/lang/core/ir/validator.cc b/src/tint/lang/core/ir/validator.cc
index d6d956b..6ea8d9f 100644
--- a/src/tint/lang/core/ir/validator.cc
+++ b/src/tint/lang/core/ir/validator.cc
@@ -413,6 +413,14 @@
     /* type_error */ "local_invocation_index must be an u32",
 };
 
+constexpr BuiltinChecker kNumSubgroupsChecker{
+    /* name */ "num_subgroups",
+    /* stages */ EnumSet<Function::PipelineStage>(Function::PipelineStage::kCompute),
+    /* direction */ IODirection::kInput,
+    /* type_check */ [](const core::type::Type* ty) -> bool { return ty->Is<core::type::U32>(); },
+    /* type_error */ "num_subgroups must be an u32",
+};
+
 constexpr BuiltinChecker kNumWorkgroupsChecker{
     /* name */ "num_workgroups",
     /* stages */ EnumSet<Function::PipelineStage>(Function::PipelineStage::kCompute),
@@ -519,6 +527,8 @@
             return kLocalInvocationIdChecker;
         case BuiltinValue::kLocalInvocationIndex:
             return kLocalInvocationIndexChecker;
+        case BuiltinValue::kNumSubgroups:
+            return kNumSubgroupsChecker;
         case BuiltinValue::kNumWorkgroups:
             return kNumWorkgroupsChecker;
         case BuiltinValue::kSampleIndex:
diff --git a/src/tint/lang/core/ir/validator_builtin_test.cc b/src/tint/lang/core/ir/validator_builtin_test.cc
index 5fbf43a..9804d4b 100644
--- a/src/tint/lang/core/ir/validator_builtin_test.cc
+++ b/src/tint/lang/core/ir/validator_builtin_test.cc
@@ -972,6 +972,53 @@
 )")) << res.Failure();
 }
 
+TEST_F(IR_ValidatorTest, Builtin_NumSubgroups_WrongStage) {
+    auto* f = VertexEntryPoint();
+    AddBuiltinParam(f, "num", BuiltinValue::kNumSubgroups, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_THAT(res.Failure().reason,
+                testing::HasSubstr(
+                    R"(:1:19 error: num_subgroups must be used in a compute shader entry point
+%f = @vertex func(%num:u32 [@num_subgroups]):vec4<f32> [@position] {
+                  ^^^^^^^^
+)")) << res.Failure();
+}
+
+TEST_F(IR_ValidatorTest, Builtin_NumSubgroups_WrongIODirection) {
+    auto* f = ComputeEntryPoint();
+    AddBuiltinReturn(f, "num", BuiltinValue::kNumSubgroups, ty.u32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_THAT(
+        res.Failure().reason,
+        testing::HasSubstr(R"(:1:1 error: num_subgroups must be an input of a shader entry point
+%f = @compute @workgroup_size(1u, 1u, 1u) func():u32 [@num_subgroups] {
+^^
+)")) << res.Failure();
+}
+
+TEST_F(IR_ValidatorTest, Builtin_NumSubgroups_WrongType) {
+    auto* f = ComputeEntryPoint();
+    AddBuiltinParam(f, "num", BuiltinValue::kNumSubgroups, ty.i32());
+
+    b.Append(f->Block(), [&] { b.Unreachable(); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_THAT(res.Failure().reason,
+                testing::HasSubstr(R"(:1:48 error: num_subgroups must be an u32
+%f = @compute @workgroup_size(1u, 1u, 1u) func(%num:i32 [@num_subgroups]):void {
+                                               ^^^^^^^^
+)")) << res.Failure();
+}
+
 TEST_F(IR_ValidatorTest, Bitcast_MissingArg) {
     auto* f = b.Function("f", ty.void_());
     b.Append(f->Block(), [&] {
diff --git a/src/tint/lang/spirv/writer/printer/printer.cc b/src/tint/lang/spirv/writer/printer/printer.cc
index ad1ea9e..feec193 100644
--- a/src/tint/lang/spirv/writer/printer/printer.cc
+++ b/src/tint/lang/spirv/writer/printer/printer.cc
@@ -393,6 +393,8 @@
             case core::BuiltinValue::kSubgroupSize:
                 module_.PushCapability(SpvCapabilityGroupNonUniform);
                 return SpvBuiltInSubgroupSize;
+            case core::BuiltinValue::kNumSubgroups:
+                TINT_UNIMPLEMENTED();
             case core::BuiltinValue::kVertexIndex:
                 return SpvBuiltInVertexIndex;
             case core::BuiltinValue::kWorkgroupId:
diff --git a/src/tint/lang/wgsl/reader/parser/variable_attribute_test.cc b/src/tint/lang/wgsl/reader/parser/variable_attribute_test.cc
index 36cdb0c..044ea55 100644
--- a/src/tint/lang/wgsl/reader/parser/variable_attribute_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/variable_attribute_test.cc
@@ -307,7 +307,7 @@
     EXPECT_EQ(attr.value, nullptr);
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), R"(1:9: expected builtin value name
-Possible values: 'barycentric_coord', 'clip_distances', 'frag_depth', 'front_facing', 'global_invocation_id', 'instance_index', 'local_invocation_id', 'local_invocation_index', 'num_workgroups', 'position', 'primitive_index', 'sample_index', 'sample_mask', 'subgroup_id', 'subgroup_invocation_id', 'subgroup_size', 'vertex_index', 'workgroup_id')");
+Possible values: 'barycentric_coord', 'clip_distances', 'frag_depth', 'front_facing', 'global_invocation_id', 'instance_index', 'local_invocation_id', 'local_invocation_index', 'num_subgroups', 'num_workgroups', 'position', 'primitive_index', 'sample_index', 'sample_mask', 'subgroup_id', 'subgroup_invocation_id', 'subgroup_size', 'vertex_index', 'workgroup_id')");
 }
 
 TEST_F(WGSLParserTest, Attribute_Builtin_MisspelledValue) {
@@ -319,7 +319,7 @@
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), R"(1:9: expected builtin value name
 Did you mean 'position'?
-Possible values: 'barycentric_coord', 'clip_distances', 'frag_depth', 'front_facing', 'global_invocation_id', 'instance_index', 'local_invocation_id', 'local_invocation_index', 'num_workgroups', 'position', 'primitive_index', 'sample_index', 'sample_mask', 'subgroup_id', 'subgroup_invocation_id', 'subgroup_size', 'vertex_index', 'workgroup_id')");
+Possible values: 'barycentric_coord', 'clip_distances', 'frag_depth', 'front_facing', 'global_invocation_id', 'instance_index', 'local_invocation_id', 'local_invocation_index', 'num_subgroups', 'num_workgroups', 'position', 'primitive_index', 'sample_index', 'sample_mask', 'subgroup_id', 'subgroup_invocation_id', 'subgroup_size', 'vertex_index', 'workgroup_id')");
 }
 
 TEST_F(WGSLParserTest, Attribute_Interpolate_Flat) {
diff --git a/src/tint/lang/wgsl/resolver/subgroups_extension_test.cc b/src/tint/lang/wgsl/resolver/subgroups_extension_test.cc
index 17de108..16ead1d 100644
--- a/src/tint/lang/wgsl/resolver/subgroups_extension_test.cc
+++ b/src/tint/lang/wgsl/resolver/subgroups_extension_test.cc
@@ -274,6 +274,84 @@
               "1:2 error: '@builtin(subgroup_id)' is only valid as a compute shader input");
 }
 
+TEST_F(ResolverSubgroupsExtensionTest, UseNumSubgroupsAttribWithoutExtensionError) {
+    Structure("Inputs",
+              Vector{
+                  Member("a", ty.u32(), Vector{Builtin(core::BuiltinValue::kNumSubgroups)}),
+              });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              "error: use of '@builtin(num_subgroups)' attribute requires enabling extension "
+              "'subgroups'");
+}
+
+TEST_F(ResolverSubgroupsExtensionTest, UseNumSubgroupsAttribWithoutLanguageExtension) {
+    Enable(wgsl::Extension::kSubgroups);
+    Structure("Inputs",
+              Vector{
+                  Member("a", ty.u32(), Vector{Builtin(core::BuiltinValue::kNumSubgroups)}),
+              });
+
+    wgsl::AllowedFeatures allowed_features{};
+    allowed_features.extensions.insert(wgsl::Extension::kSubgroups);
+    Resolver resolver{this, allowed_features};
+    EXPECT_FALSE(resolver.Resolve());
+    EXPECT_EQ(resolver.error(),
+              "error: use of '@builtin(num_subgroups)' attribute requires the 'subgroup_id' "
+              "language feature");
+}
+
+TEST_F(ResolverSubgroupsExtensionTest, UseNumSubgroupsAttribWithExtension) {
+    Enable(wgsl::Extension::kSubgroups);
+    Structure("Inputs",
+              Vector{
+                  Member("a", ty.u32(), Vector{Builtin(core::BuiltinValue::kNumSubgroups)}),
+              });
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverSubgroupsExtensionTest, NumSubgroupsI32Error) {
+    Enable(wgsl::Extension::kSubgroups);
+    Structure("Inputs",
+              Vector{
+                  Member("a", ty.i32(), Vector{Builtin(core::BuiltinValue::kNumSubgroups)}),
+              });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "error: store type of '@builtin(num_subgroups)' must be 'u32'");
+}
+
+TEST_F(ResolverSubgroupsExtensionTest, NumSubgroupsFragmentShader) {
+    Enable(wgsl::Extension::kSubgroups);
+    Func("main",
+         Vector{Param("size", ty.u32(), Vector{Builtin(core::BuiltinValue::kNumSubgroups)})},
+         ty.void_(), Empty, Vector{Stage(ast::PipelineStage::kFragment)});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              "error: '@builtin(num_subgroups)' is only valid as a compute shader input");
+}
+
+TEST_F(ResolverSubgroupsExtensionTest, NumSubgroupsComputeShaderOutput) {
+    Enable(wgsl::Extension::kSubgroups);
+
+    Func("main", tint::Empty, ty.u32(),
+         Vector{
+             Return(Call<u32>()),
+         },
+         Vector{
+             Stage(ast::PipelineStage::kCompute),
+             WorkgroupSize(1_i),
+         },
+         Vector{Builtin(Source{{1, 2}}, core::BuiltinValue::kNumSubgroups)});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              "1:2 error: '@builtin(num_subgroups)' is only valid as a compute shader input");
+}
+
 // Using the subgroup_uniformity diagnostic rule without subgroups enabled should succeed.
 TEST_F(ResolverSubgroupsExtensionTest, UseSubgroupUniformityRuleWithoutExtensionError) {
     DiagnosticDirective(wgsl::DiagnosticSeverity::kOff, "subgroup_uniformity");
diff --git a/src/tint/lang/wgsl/resolver/validator.cc b/src/tint/lang/wgsl/resolver/validator.cc
index b574b29..e6a3809 100644
--- a/src/tint/lang/wgsl/resolver/validator.cc
+++ b/src/tint/lang/wgsl/resolver/validator.cc
@@ -1125,6 +1125,34 @@
                 return false;
             }
             break;
+        case core::BuiltinValue::kNumSubgroups:
+            if (!enabled_extensions_.Contains(wgsl::Extension::kSubgroups)) {
+                AddError(attr->source)
+                    << "use of " << style::Attribute("@builtin")
+                    << style::Code("(", style::Enum(builtin), ")")
+                    << " attribute requires enabling extension " << style::Code("subgroups");
+                return false;
+            }
+            // TODO(crbug.com/454654105): Remove this check.
+            if (!allowed_features_.features.contains(wgsl::LanguageFeature::kSubgroupId)) {
+                AddError(attr->source)
+                    << "use of " << style::Attribute("@builtin")
+                    << style::Code("(", style::Enum(builtin), ")") << " attribute requires the "
+                    << style::Code("subgroup_id") << " language feature";
+                return false;
+            }
+            if (!type->Is<core::type::U32>()) {
+                err_builtin_type("u32");
+                return false;
+            }
+            if (stage != ast::PipelineStage::kNone &&
+                !(stage == ast::PipelineStage::kCompute && is_input)) {
+                AddError(attr->source)
+                    << style::Attribute("@builtin") << style::Code("(", style::Enum(builtin), ")")
+                    << " is only valid as a compute shader input";
+                return false;
+            }
+            break;
         case core::BuiltinValue::kSubgroupInvocationId:
         case core::BuiltinValue::kSubgroupSize:
             if (!enabled_extensions_.Contains(wgsl::Extension::kSubgroups)) {
diff --git a/src/tint/utils/protos/ir/ir.proto b/src/tint/utils/protos/ir/ir.proto
index 0f4c772..fe63070 100644
--- a/src/tint/utils/protos/ir/ir.proto
+++ b/src/tint/utils/protos/ir/ir.proto
@@ -584,6 +584,7 @@
     subgroup_id = 17;
     primitive_index = 18;
     barycentric_coord = 19;
+    num_subgroups = 20;
 }
 
 enum BuiltinFn {