[tint][intrinsics] Simplify intrinsic table

* Combine the type and number matcher index tables. The usage knows which
  kind of matcher is needed.
* Combine the type and number template structures.
* Change the template matcher from a single matcher index, to a list of
  matchers. This allows templates to match using other templates.
* Remove the validation forbidding the direct use of a matcher in a
  parameter. While this is discouraged for WGSL due to readability of
  diagnostics messages, there's no reason this couldn't be used to
  simplify other definition files.

Change-Id: I86897d49297c3d4b960e1f73c465103593a445d4
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/175242
Commit-Queue: Ben Clayton <bclayton@google.com>
Auto-Submit: Ben Clayton <bclayton@google.com>
Commit-Queue: dan sinclair <dsinclair@chromium.org>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/tools/src/cmd/gen/templates/templates.go b/tools/src/cmd/gen/templates/templates.go
index ebe6652..783b4ed 100644
--- a/tools/src/cmd/gen/templates/templates.go
+++ b/tools/src/cmd/gen/templates/templates.go
@@ -249,7 +249,11 @@
 			return nil, err
 		}
 	}
-	return i.cachedPermuter.Permute(overload)
+	out, err := i.cachedPermuter.Permute(overload)
+	if err != nil {
+		return nil, fmt.Errorf("while permuting '%v'\n%w", overload, err)
+	}
+	return out, nil
 }
 
 // Cache for objects that are expensive to build, and can be reused between templates.
diff --git a/tools/src/template/template.go b/tools/src/template/template.go
index fcd14a1..5af3ef6 100644
--- a/tools/src/template/template.go
+++ b/tools/src/template/template.go
@@ -159,7 +159,7 @@
 	}
 
 	if err != nil {
-		return "", fmt.Errorf("while evaluating '%v': %v", template, err)
+		return "", fmt.Errorf("while evaluating '%v' with args '%v'\n%v", template, args, err)
 	}
 	return sb.String(), nil
 }
diff --git a/tools/src/tint/intrinsic/ast/ast.go b/tools/src/tint/intrinsic/ast/ast.go
index 350009a..4059fec 100644
--- a/tools/src/tint/intrinsic/ast/ast.go
+++ b/tools/src/tint/intrinsic/ast/ast.go
@@ -150,7 +150,7 @@
 	Kind           IntrinsicKind
 	Name           string
 	Attributes     Attributes
-	TemplateParams TemplateParams
+	TemplateParams []TemplateParam
 	Parameters     Parameters
 	ReturnType     *TemplatedName
 }
@@ -167,8 +167,13 @@
 	case Converter:
 		fmt.Fprintf(w, "conv ")
 	}
+
 	fmt.Fprintf(w, "%v", i.Name)
-	i.TemplateParams.Format(w, verb)
+	if len(i.TemplateParams) > 0 {
+		fmt.Fprintf(w, "<")
+		formatList(w, i.TemplateParams)
+		fmt.Fprintf(w, ">")
+	}
 	i.Parameters.Format(w, verb)
 	if i.ReturnType != nil {
 		fmt.Fprintf(w, " -> ")
@@ -238,12 +243,7 @@
 
 // Format implements the fmt.Formatter interface
 func (l TemplatedNames) Format(w fmt.State, verb rune) {
-	for i, n := range l {
-		if i > 0 {
-			fmt.Fprintf(w, ", ")
-		}
-		n.Format(w, verb)
-	}
+	formatList(w, l)
 }
 
 // TemplatedName is an identifier with optional templated arguments
@@ -274,12 +274,7 @@
 
 // Format implements the fmt.Formatter interface
 func (l MemberNames) Format(w fmt.State, verb rune) {
-	for i, n := range l {
-		if i > 0 {
-			fmt.Fprintf(w, ", ")
-		}
-		n.Format(w, verb)
-	}
+	formatList(w, l)
 }
 
 // MemberName is two identifiers separated by a dot (Owner.Member)
@@ -299,7 +294,7 @@
 	Source         tok.Source
 	Attributes     Attributes
 	Name           string
-	TemplateParams TemplateParams
+	TemplateParams []TemplateParam
 }
 
 // Format implements the fmt.Formatter interface
@@ -309,25 +304,9 @@
 		fmt.Fprintf(w, " type %v", p.Name)
 	}
 	fmt.Fprintf(w, "type %v", p.Name)
-	p.TemplateParams.Format(w, verb)
-}
-
-// TemplateParams is a list of TemplateParam
-// Example:
-//
-//	<A, B : TyB>
-type TemplateParams []TemplateParam
-
-// Format implements the fmt.Formatter interface
-func (p TemplateParams) Format(w fmt.State, verb rune) {
-	if len(p) > 0 {
+	if len(p.TemplateParams) > 0 {
 		fmt.Fprintf(w, "<")
-		for i, tp := range p {
-			if i > 0 {
-				fmt.Fprintf(w, ", ")
-			}
-			tp.Format(w, verb)
-		}
+		formatList(w, p.TemplateParams)
 		fmt.Fprintf(w, ">")
 	}
 }
@@ -395,12 +374,17 @@
 	fmt.Fprintf(w, "%v", d.Name)
 	if len(d.Values) > 0 {
 		fmt.Fprintf(w, "(")
-		for i, v := range d.Values {
-			if i > 0 {
-				fmt.Fprint(w, ", ")
-			}
-			fmt.Fprintf(w, "%v", v)
-		}
+		formatList(w, d.Values)
 		fmt.Fprintf(w, ")")
 	}
 }
+
+// formatList writes the comma separated list to w.
+func formatList[T any](w fmt.State, list []T) {
+	for i, v := range list {
+		if i > 0 {
+			fmt.Fprint(w, ", ")
+		}
+		fmt.Fprintf(w, "%v", v)
+	}
+}
diff --git a/tools/src/tint/intrinsic/gen/gen.go b/tools/src/tint/intrinsic/gen/gen.go
index 7a98730..0037252 100644
--- a/tools/src/tint/intrinsic/gen/gen.go
+++ b/tools/src/tint/intrinsic/gen/gen.go
@@ -54,35 +54,30 @@
 	NMatchers     []sem.Named
 	NMatcherIndex map[sem.Named]int // [object -> index] in NMatchers
 
-	TypeMatcherIndices        []int            // kTypeMatcherIndices table content
-	NumberMatcherIndices      []int            // kNumberMatcherIndices table content
-	TemplateTypes             []TemplateType   // kTemplateTypes table content
-	TemplateNumbers           []TemplateNumber // kTemplateNumbers table content
-	Parameters                []Parameter      // kParameters table content
-	Overloads                 []Overload       // kOverloads table content
-	Builtins                  []Intrinsic      // kBuiltins table content
-	UnaryOperators            []Intrinsic      // kUnaryOperators table content
-	BinaryOperators           []Intrinsic      // kBinaryOperators table content
-	ConstructorsAndConverters []Intrinsic      // kInitializersAndConverters table content
-	ConstEvalFunctions        []string         // kConstEvalFunctions table content
+	MatcherIndices            []int       // kMatcherIndices table content
+	Templates                 []Template  // kTemplates table content
+	Parameters                []Parameter // kParameters table content
+	Overloads                 []Overload  // kOverloads table content
+	Builtins                  []Intrinsic // kBuiltins table content
+	UnaryOperators            []Intrinsic // kUnaryOperators table content
+	BinaryOperators           []Intrinsic // kBinaryOperators table content
+	ConstructorsAndConverters []Intrinsic // kInitializersAndConverters table content
+	ConstEvalFunctions        []string    // kConstEvalFunctions table content
 }
 
-// TemplateType is used to create the C++ TemplateTypeInfo structure
-type TemplateType struct {
+// Template is used to create the C++ TemplateInfo structure
+type Template struct {
 	// Name of the template type (e.g. 'T')
 	Name string
-	// Optional type matcher constraint.
-	// Either an index in Matchers::type, or -1
-	MatcherIndex int
-}
 
-// TemplateNumber is used to create the C++ TemplateNumberInfo structure
-type TemplateNumber struct {
-	// Name of the template number (e.g. 'N')
-	Name string
-	// Optional type matcher constraint.
-	// Either an index in Matchers::type, or -1
-	MatcherIndex int
+	// Kind of the template
+	Kind sem.TemplateKind
+
+	// Index into IntrinsicTable.MatcherIndices, beginning the list of matchers required to match
+	// the parameter type.
+	// The matcher indices index into IntrinsicTable::TMatchers and IntrinsicTable::NMatchers.
+	// These indices are consumed by the matchers themselves.
+	MatcherIndicesOffset int
 }
 
 // Parameter is used to create the C++ ParameterInfo structure
@@ -90,41 +85,28 @@
 	// The parameter usage (parameter name)
 	Usage string
 
-	// Index into IntrinsicTable.TypeMatcherIndices, beginning the list of matchers
-	// required to match the parameter type.
-	// The matcher indices index into IntrinsicTable::TMatchers.
+	// Index into IntrinsicTable.MatcherIndices, beginning the list of matchers required to match
+	// the parameter type.
+	// The matcher indices index into IntrinsicTable::TMatchers and IntrinsicTable::NMatchers.
 	// These indices are consumed by the matchers themselves.
-	TypeMatcherIndicesOffset int
-
-	// Index into IntrinsicTable.NumberMatcherIndices.
-	// The matcher indices index into IntrinsicTable::NMatchers.
-	// These indices are consumed by the matchers themselves.
-	NumberMatcherIndicesOffset int
+	MatcherIndicesOffset int
 }
 
 // Overload is used to create the C++ OverloadInfo structure
 type Overload struct {
 	// Total number of parameters for the overload
 	NumParameters int
-	// Total number of template types for the overload
-	NumTemplateTypes int
-	// Total number of template numbers for the overload
-	NumTemplateNumbers int
-	// Index to the first template type in IntrinsicTable.TemplateTypes
-	TemplateTypesOffset int
-	// Index to the first template number in IntrinsicTable.TemplateNumbers
-	TemplateNumbersOffset int
+	// Total number of templates for the overload
+	NumTemplates int
+	// Index to the first template in IntrinsicTable.Templates
+	TemplatesOffset int
 	// Index to the first parameter in IntrinsicTable.Parameters
 	ParametersOffset int
-	// Index into IntrinsicTable.TypeMatcherIndices, beginning the list of matchers
+	// Index into IntrinsicTable.matcherIndices, beginning the list of matchers
 	// required to match the return type.
 	// The matcher indices index into IntrinsicTable::TMatchers.
 	// These indices are consumed by the matchers themselves.
-	ReturnTypeMatcherIndicesOffset int
-	// Index into IntrinsicTable.NumberMatcherIndices.
-	// The matcher indices index into IntrinsicTable::NMatchers.
-	// These indices are consumed by the matchers themselves.
-	ReturnNumberMatcherIndicesOffset int
+	ReturnMatcherIndicesOffset int
 	// Index into IntrinsicTable.ConstEvalFunctions.
 	ConstEvalFunctionOffset int
 	// StageUses describes the stages an overload can be used in
@@ -153,10 +135,8 @@
 	// Lookup tables.
 	// These are packed (compressed) once all the entries have been added.
 	lut struct {
-		typeMatcherIndices       lut.LUT[int]
-		numberMatcherIndices     lut.LUT[int]
-		templateTypes            lut.LUT[TemplateType]
-		templateNumbers          lut.LUT[TemplateNumber]
+		matcherIndices           lut.LUT[int]
+		templates                lut.LUT[Template]
 		constEvalFunctionIndices lut.LUT[string]
 		parameters               lut.LUT[Parameter]
 		overloads                lut.LUT[Overload]
@@ -164,9 +144,15 @@
 }
 
 type parameterBuilder struct {
-	usage                      string
-	typeMatcherIndicesOffset   *int
-	numberMatcherIndicesOffset *int
+	usage                string
+	matcherIndicesOffset *int
+}
+
+type templateBuilder struct {
+	// The index of the template in kTypeMatchers / kNumberMatchers
+	matcherIndex int
+	// The matcher indices for this template type / number
+	constraintIndicesOffset *int
 }
 
 // Helper for building a single overload
@@ -174,39 +160,31 @@
 	*IntrinsicTableBuilder
 	// The overload being built
 	overload *sem.Overload
-	// Maps TemplateParam to index in templateTypes
-	templateTypeIndex map[sem.TemplateParam]int
-	// Maps TemplateParam to index in templateNumbers
-	templateNumberIndex map[sem.TemplateParam]int
-	// Template types used by the overload
-	templateTypes []TemplateType
-	// Index to the first template type in IntrinsicTable.TemplateTypes
-	templateTypesOffset *int
-	// Template numbers used by the overload
-	templateNumbers []TemplateNumber
-	// Index to the first template number in IntrinsicTable.TemplateNumbers
-	templateNumbersOffset *int
+	// Map of TemplateParam to templatesBuilder
+	templateBuilders map[sem.TemplateParam]*templateBuilder
+	// Templates used by the overload
+	templates []Template
+	// Index to the first template in IntrinsicTable.Templates
+	templateOffset *int
 	// Builders for all parameters
 	parameterBuilders []parameterBuilder
 	// Index to the first parameter in IntrinsicTable.Parameters
 	parametersOffset *int
 	// Index into IntrinsicTable.ConstEvalFunctions
 	constEvalFunctionOffset *int
-	// Index into IntrinsicTable.TypeMatcherIndices, beginning the list of
+	// Index into IntrinsicTable.matcherIndices, beginning the list of
 	// matchers required to match the return type.
 	// The matcher indices index into IntrinsicTable::TMatchers.
 	// These indices are consumed by the matchers themselves.
-	returnTypeMatcherIndicesOffset *int
-	// Index into IntrinsicTable.NumberMatcherIndices.
-	// The matcher indices index into IntrinsicTable::NMatchers.
-	// These indices are consumed by the matchers themselves.
-	returnNumberMatcherIndicesOffset *int
+	returnMatcherIndicesOffset *int
 }
 
 // layoutMatchers assigns each of the TMatchers and NMatchers a unique index.
 func (b *IntrinsicTableBuilder) layoutMatchers(s *sem.Sem) {
-	// First MaxTemplateTypes of TMatchers are template types
-	b.TMatchers = make([]sem.Named, s.MaxTemplateTypes)
+	// First MaxTemplates of TMatchers and NMatchers are template types
+	b.TMatchers = make([]sem.Named, s.MaxTemplates)
+	b.NMatchers = make([]sem.Named, s.MaxTemplates)
+
 	for _, m := range s.Types {
 		b.TMatcherIndex[m] = len(b.TMatchers)
 		b.TMatchers = append(b.TMatchers, m)
@@ -215,9 +193,6 @@
 		b.TMatcherIndex[m] = len(b.TMatchers)
 		b.TMatchers = append(b.TMatchers, m)
 	}
-
-	// First MaxTemplateNumbers of NMatchers are template numbers
-	b.NMatchers = make([]sem.Named, s.MaxTemplateNumbers)
 	for _, m := range s.EnumMatchers {
 		b.NMatcherIndex[m] = len(b.NMatchers)
 		b.NMatchers = append(b.NMatchers, m)
@@ -228,8 +203,7 @@
 	return &overloadBuilder{
 		IntrinsicTableBuilder: b,
 		overload:              o,
-		templateTypeIndex:     map[sem.TemplateParam]int{},
-		templateNumberIndex:   map[sem.TemplateParam]int{},
+		templateBuilders:      map[sem.TemplateParam]*templateBuilder{},
 	}
 }
 
@@ -237,77 +211,55 @@
 // Preconditions:
 // - Must be called before any LUTs are compacted.
 // Populates:
-// - b.templateTypes
-// - b.templateTypesOffset
-// - b.templateNumbers
-// - b.templateNumbersOffset
+// - b.templateBuilders
 // - b.parameterBuilders
-// - b.returnTypeMatcherIndicesOffset
-// - b.returnNumberMatcherIndicesOffset
+// - b.returnMatcherIndicesOffset
 // - b.constEvalFunctionOffset
 func (b *overloadBuilder) processStage0() error {
-	b.templateTypes = make([]TemplateType, len(b.overload.TemplateTypes))
-	for i, t := range b.overload.TemplateTypes {
-		b.templateTypeIndex[t] = i
-		matcherIndex := -1
-		if t.Type != nil {
-			tys, nums, err := b.matcherIndices(t.Type)
-			if err != nil {
-				return err
-			}
-			if len(tys) != 1 || len(nums) != 0 {
-				panic("unexpected result of matcherIndices()")
-			}
-			matcherIndex = tys[0]
-		}
-		b.templateTypes[i] = TemplateType{
-			Name:         t.Name,
-			MatcherIndex: matcherIndex,
-		}
+	// Calculate the template matcher indices
+	for _, t := range b.overload.Templates {
+		b.templateBuilders[t] = &templateBuilder{matcherIndex: len(b.templateBuilders)}
 	}
-	b.templateTypesOffset = b.lut.templateTypes.Add(b.templateTypes)
 
-	b.templateNumbers = make([]TemplateNumber, len(b.overload.TemplateNumbers))
-	for i, t := range b.overload.TemplateNumbers {
-		b.templateNumberIndex[t] = i
-		matcherIndex := -1
-		if e, ok := t.(*sem.TemplateEnumParam); ok && e.Matcher != nil {
-			tys, nums, err := b.matcherIndices(e.Matcher)
-			if err != nil {
-				return err
+	for _, t := range b.overload.Templates {
+		switch t := t.(type) {
+		case *sem.TemplateTypeParam:
+			if t.Type != nil {
+				indices, err := b.collectMatcherIndices(*t.Type)
+				if err != nil {
+					return err
+				}
+				b.templateBuilders[t].constraintIndicesOffset = b.lut.matcherIndices.Add(indices)
 			}
-			if len(tys) != 0 || len(nums) != 1 {
-				panic("unexpected result of matcherIndices()")
+		case *sem.TemplateEnumParam:
+			if t.Matcher != nil {
+				index, err := b.matcherIndex(t.Matcher)
+				if err != nil {
+					return err
+				}
+				b.templateBuilders[t].constraintIndicesOffset = b.lut.matcherIndices.Add([]int{index})
 			}
-			matcherIndex = nums[0]
-		}
-		b.templateNumbers[i] = TemplateNumber{
-			Name:         t.GetName(),
-			MatcherIndex: matcherIndex,
 		}
 	}
-	b.templateNumbersOffset = b.lut.templateNumbers.Add(b.templateNumbers)
 
 	if b.overload.ReturnType != nil {
-		typeIndices, numberIndices, err := b.collectMatcherIndices(*b.overload.ReturnType)
+		indices, err := b.collectMatcherIndices(*b.overload.ReturnType)
 		if err != nil {
 			return err
 		}
-		b.returnTypeMatcherIndicesOffset = b.lut.typeMatcherIndices.Add(typeIndices)
-		b.returnNumberMatcherIndicesOffset = b.lut.numberMatcherIndices.Add(numberIndices)
+		b.returnMatcherIndicesOffset = b.lut.matcherIndices.Add(indices)
 	}
 
 	b.parameterBuilders = make([]parameterBuilder, len(b.overload.Parameters))
 	for i, p := range b.overload.Parameters {
-		typeIndices, numberIndices, err := b.collectMatcherIndices(p.Type)
+		matcherIndices, err := b.collectMatcherIndices(p.Type)
 		if err != nil {
 			return err
 		}
 
 		b.parameterBuilders[i] = parameterBuilder{
-			usage:                      p.Name,
-			typeMatcherIndicesOffset:   b.lut.typeMatcherIndices.Add(typeIndices),
-			numberMatcherIndicesOffset: b.lut.numberMatcherIndices.Add(numberIndices),
+			usage:                p.Name,
+			matcherIndicesOffset: b.lut.matcherIndices.Add(matcherIndices),
 		}
 	}
 
@@ -320,19 +272,27 @@
 
 // processStage1 builds the Parameters  used by the overload
 // Must only be called after the following LUTs have been compacted:
-// - b.lut.typeMatcherIndices
-// - b.lut.numberMatcherIndices
-// - b.lut.templateTypes
-// - b.lut.templateNumbers
+// - b.lut.matcherIndices
 // Populates:
+// - b.templates
+// - b.templateOffset
 // - b.parametersOffset
 func (b *overloadBuilder) processStage1() error {
+	b.templates = []Template{}
+	for _, t := range b.overload.Templates {
+		b.templates = append(b.templates, Template{
+			Name:                 t.GetName(),
+			Kind:                 t.TemplateKind(),
+			MatcherIndicesOffset: loadOrMinusOne(b.templateBuilders[t].constraintIndicesOffset),
+		})
+	}
+	b.templateOffset = b.lut.templates.Add(b.templates)
+
 	parameters := make([]Parameter, len(b.parameterBuilders))
 	for i, pb := range b.parameterBuilders {
 		parameters[i] = Parameter{
-			Usage:                      pb.usage,
-			TypeMatcherIndicesOffset:   loadOrMinusOne(pb.typeMatcherIndicesOffset),
-			NumberMatcherIndicesOffset: loadOrMinusOne(pb.numberMatcherIndicesOffset),
+			Usage:                pb.usage,
+			MatcherIndicesOffset: loadOrMinusOne(pb.matcherIndicesOffset),
 		}
 	}
 	b.parametersOffset = b.lut.parameters.Add(parameters)
@@ -341,53 +301,40 @@
 
 func (b *overloadBuilder) build() (Overload, error) {
 	return Overload{
-		NumParameters:                    len(b.parameterBuilders),
-		NumTemplateTypes:                 len(b.templateTypes),
-		NumTemplateNumbers:               len(b.templateNumbers),
-		TemplateTypesOffset:              loadOrMinusOne(b.templateTypesOffset),
-		TemplateNumbersOffset:            loadOrMinusOne(b.templateNumbersOffset),
-		ParametersOffset:                 loadOrMinusOne(b.parametersOffset),
-		ConstEvalFunctionOffset:          loadOrMinusOne(b.constEvalFunctionOffset),
-		ReturnTypeMatcherIndicesOffset:   loadOrMinusOne(b.returnTypeMatcherIndicesOffset),
-		ReturnNumberMatcherIndicesOffset: loadOrMinusOne(b.returnNumberMatcherIndicesOffset),
-		CanBeUsedInStage:                 b.overload.CanBeUsedInStage,
-		MustUse:                          b.overload.MustUse,
-		IsDeprecated:                     b.overload.IsDeprecated,
-		Kind:                             string(b.overload.Decl.Kind),
+		NumParameters:              len(b.parameterBuilders),
+		NumTemplates:               len(b.overload.Templates),
+		TemplatesOffset:            loadOrMinusOne(b.templateOffset),
+		ParametersOffset:           loadOrMinusOne(b.parametersOffset),
+		ConstEvalFunctionOffset:    loadOrMinusOne(b.constEvalFunctionOffset),
+		ReturnMatcherIndicesOffset: loadOrMinusOne(b.returnMatcherIndicesOffset),
+		CanBeUsedInStage:           b.overload.CanBeUsedInStage,
+		MustUse:                    b.overload.MustUse,
+		IsDeprecated:               b.overload.IsDeprecated,
+		Kind:                       string(b.overload.Decl.Kind),
 	}, nil
 }
 
 // matcherIndex returns the matcher indices into IntrinsicTable.TMatcher and
 // IntrinsicTable.NMatcher, respectively for the given named entity.
-func (b *overloadBuilder) matcherIndices(n sem.Named) (types, numbers []int, err error) {
+func (b *overloadBuilder) matcherIndex(n sem.Named) (int, error) {
 	switch n := n.(type) {
 	case *sem.Type, *sem.TypeMatcher:
 		if i, ok := b.TMatcherIndex[n]; ok {
-			return []int{i}, nil, nil
+			return i, nil
 		}
-		return nil, nil, fmt.Errorf("matcherIndex missing entry for %v %T", n.GetName(), n)
-	case *sem.TemplateTypeParam:
-		if i, ok := b.templateTypeIndex[n]; ok {
-			return []int{i}, nil, nil
-		}
-		return nil, nil, fmt.Errorf("templateTypeIndex missing entry for %v %T", n.Name, n)
+		return -1, fmt.Errorf("TMatcherIndex missing entry for %v %T", n.GetName(), n)
 	case *sem.EnumMatcher:
 		if i, ok := b.NMatcherIndex[n]; ok {
-			return nil, []int{i}, nil
+			return i, nil
 		}
-		return nil, nil, fmt.Errorf("matcherIndex missing entry for %v %T", n.GetName(), n)
-	case *sem.TemplateEnumParam:
-		if i, ok := b.templateNumberIndex[n]; ok {
-			return nil, []int{i}, nil
+		return -1, fmt.Errorf("NMatcherIndex missing entry for %v %T", n.GetName(), n)
+	case sem.TemplateParam:
+		if b, ok := b.templateBuilders[n]; ok {
+			return b.matcherIndex, nil
 		}
-		return nil, nil, fmt.Errorf("templateNumberIndex missing entry for %v %T", n, n)
-	case *sem.TemplateNumberParam:
-		if i, ok := b.templateNumberIndex[n]; ok {
-			return nil, []int{i}, nil
-		}
-		return nil, nil, fmt.Errorf("templateNumberIndex missing entry for %v %T", n, n)
+		return -1, fmt.Errorf("templatesBuilders missing entry for %v %T", n.GetName(), n)
 	default:
-		return nil, nil, fmt.Errorf("overload.matcherIndices() does not handle %v %T", n, n)
+		return -1, fmt.Errorf("overload.matcherIndices() does not handle %v %T", n, n)
 	}
 }
 
@@ -408,25 +355,24 @@
 // Would return the matcher indices:
 //
 //	A, B, C, D, E, F, G, H, I
-func (b *overloadBuilder) collectMatcherIndices(fqn sem.FullyQualifiedName) (tys, nums []int, err error) {
-	tys, nums, err = b.matcherIndices(fqn.Target)
+func (b *overloadBuilder) collectMatcherIndices(fqn sem.FullyQualifiedName) ([]int, error) {
+	base, err := b.matcherIndex(fqn.Target)
 	if err != nil {
-		return nil, nil, err
+		return nil, err
 	}
+	indices := []int{base}
 	for _, arg := range fqn.TemplateArguments {
-		typeIndices, numberIndices, err := b.collectMatcherIndices(arg.(sem.FullyQualifiedName))
+		subIndices, err := b.collectMatcherIndices(arg.(sem.FullyQualifiedName))
 		if err != nil {
-			return nil, nil, err
+			return nil, err
 		}
-		tys = append(tys, typeIndices...)
-		nums = append(nums, numberIndices...)
+		indices = append(indices, subIndices...)
 	}
-	return tys, nums, nil
+	return indices, nil
 }
 
 // BuildIntrinsicTable builds the IntrinsicTable from the semantic info
 func BuildIntrinsicTable(s *sem.Sem) (*IntrinsicTable, error) {
-
 	b := IntrinsicTableBuilder{
 		IntrinsicTable: IntrinsicTable{
 			Sem:           s,
@@ -460,35 +406,32 @@
 	}
 
 	// Perform the 'stage-0' processing of the overloads
-	b.lut.typeMatcherIndices = lut.New[int]()
-	b.lut.numberMatcherIndices = lut.New[int]()
-	b.lut.templateTypes = lut.New[TemplateType]()
-	b.lut.templateNumbers = lut.New[TemplateNumber]()
+	b.lut.matcherIndices = lut.New[int]()
 	b.lut.constEvalFunctionIndices = lut.New[string]()
 	for _, b := range overloadBuilders {
-		b.processStage0()
+		if err := b.processStage0(); err != nil {
+			return nil, fmt.Errorf("while processing stage 0 of '%v'\n%w", b.overload, err)
+		}
 	}
 
-	// Compact type and number LUTs
-	b.TypeMatcherIndices = b.lut.typeMatcherIndices.Compact()
-	b.NumberMatcherIndices = b.lut.numberMatcherIndices.Compact()
-	b.TemplateTypes = b.lut.templateTypes.Compact()
-	b.TemplateNumbers = b.lut.templateNumbers.Compact()
-	b.ConstEvalFunctions = b.lut.constEvalFunctionIndices.Compact()
 	// Clear the compacted LUTs to prevent use-after-compaction
-	b.lut.typeMatcherIndices = nil
-	b.lut.numberMatcherIndices = nil
-	b.lut.templateTypes = nil
-	b.lut.templateNumbers = nil
+	b.MatcherIndices = b.lut.matcherIndices.Compact()
+	b.ConstEvalFunctions = b.lut.constEvalFunctionIndices.Compact()
+	b.lut.matcherIndices = nil
 	b.lut.constEvalFunctionIndices = nil
+	b.lut.templates = lut.New[Template]()
 
 	// Perform the 'stage-1' processing of the overloads
 	b.lut.parameters = lut.New[Parameter]()
 	for _, b := range overloadBuilders {
-		b.processStage1()
+		if err := b.processStage1(); err != nil {
+			return nil, fmt.Errorf("while processing stage 1 of '%v'\n%w", b.overload, err)
+		}
 	}
 	b.Parameters = b.lut.parameters.Compact()
+	b.Templates = b.lut.templates.Compact()
 	b.lut.parameters = nil
+	b.lut.templates = nil
 
 	// Build the Intrinsics
 	b.lut.overloads = lut.New[Overload]()
diff --git a/tools/src/tint/intrinsic/gen/permutate.go b/tools/src/tint/intrinsic/gen/permutate.go
index 47d7aa8..7d35a19 100644
--- a/tools/src/tint/intrinsic/gen/permutate.go
+++ b/tools/src/tint/intrinsic/gen/permutate.go
@@ -63,7 +63,7 @@
 
 // Permutation describes a single permutation of an overload
 type Permutation struct {
-	sem.Overload        // The permutated overload signature
+	sem.Overload        // The permuted overload signature
 	Desc         string // Description of the overload
 	Hash         string // Hash of the overload
 }
@@ -82,9 +82,10 @@
 	// Map of hash to permutation description. Used to detect collisions.
 	hashes := map[string]string{}
 
-	// permutate appends a permutation to out.
-	// permutate may be chained to generate N-dimensional permutations.
-	permutate := func() error {
+	// permute appends a permutation to out.
+	// permute may be chained to generate N-dimensional permutations.
+	permute := func() error {
+		// Generate an overload with the templated types replaced with the permuted types.
 		o := sem.Overload{
 			Decl:             overload.Decl,
 			Intrinsic:        overload.Intrinsic,
@@ -103,15 +104,16 @@
 			})
 		}
 		if overload.ReturnType != nil {
-			retTys, err := state.permutateFQN(*overload.ReturnType)
+			retTys, err := state.permuteFQN(*overload.ReturnType)
 			if err != nil {
-				return fmt.Errorf("while permutating return type: %w", err)
+				return fmt.Errorf("while permuting return type: %w", err)
 			}
 			if len(retTys) != 1 {
 				return fmt.Errorf("result type not pinned")
 			}
 			o.ReturnType = &retTys[0]
 		}
+
 		desc := fmt.Sprint(o)
 		hash := sha256.Sum256([]byte(desc))
 		const hashLength = 6
@@ -136,9 +138,9 @@
 	}
 	for i, param := range overload.Parameters {
 		i, param := i, param // Capture iterator values for anonymous function
-		next := permutate    // Permutation chaining
-		permutate = func() error {
-			permutations, err := state.permutateFQN(param.Type)
+		next := permute      // Permutation chaining
+		permute = func() error {
+			permutations, err := state.permuteFQN(param.Type)
 			if err != nil {
 				return fmt.Errorf("while processing parameter %v: %w", i, err)
 			}
@@ -154,22 +156,24 @@
 			return nil
 		}
 	}
-	for _, t := range overload.TemplateParams {
-		next := permutate // Permutation chaining
+
+	for _, t := range overload.Templates {
+		t := t          // Capture iterator values for anonymous function
+		next := permute // Permutation chaining
 		switch t := t.(type) {
 		case *sem.TemplateTypeParam:
-			types := p.allTypes
-			if t.Type != nil {
-				var err error
-				types, err = state.permutateFQN(sem.FullyQualifiedName{Target: t.Type})
-				if err != nil {
-					return nil, fmt.Errorf("while permutating template types: %w", err)
+			permute = func() error {
+				types := p.allTypes
+				if t.Type != nil {
+					var err error
+					types, err = state.permuteFQN(*t.Type)
+					if err != nil {
+						return fmt.Errorf("while permuting template types: %w", err)
+					}
 				}
-			}
-			if len(types) == 0 {
-				return nil, fmt.Errorf("template type %v has no permutations", t.Name)
-			}
-			permutate = func() error {
+				if len(types) == 0 {
+					return fmt.Errorf("template type %v has no permutations", t.Name)
+				}
 				for _, ty := range types {
 					state.templateTypes[t] = ty
 					if err := next(); err != nil {
@@ -182,17 +186,17 @@
 			var permutations []sem.FullyQualifiedName
 			var err error
 			if t.Matcher != nil {
-				permutations, err = state.permutateFQN(sem.FullyQualifiedName{Target: t.Matcher})
+				permutations, err = state.permuteFQN(sem.FullyQualifiedName{Target: t.Matcher})
 			} else {
-				permutations, err = state.permutateFQN(sem.FullyQualifiedName{Target: t.Enum})
+				permutations, err = state.permuteFQN(sem.FullyQualifiedName{Target: t.Enum})
 			}
 			if err != nil {
-				return nil, fmt.Errorf("while permutating template numbers: %w", err)
+				return nil, fmt.Errorf("while permuting template numbers: %w", err)
 			}
 			if len(permutations) == 0 {
 				return nil, fmt.Errorf("template type %v has no permutations", t.Name)
 			}
-			permutate = func() error {
+			permute = func() error {
 				for _, n := range permutations {
 					state.templateNumbers[t] = n
 					if err := next(); err != nil {
@@ -204,7 +208,7 @@
 		case *sem.TemplateNumberParam:
 			// Currently all open numbers are used for vector / matrices
 			permutations := []int{2, 3, 4}
-			permutate = func() error {
+			permute = func() error {
 				for _, n := range permutations {
 					state.templateNumbers[t] = n
 					if err := next(); err != nil {
@@ -216,7 +220,7 @@
 		}
 	}
 
-	if err := permutate(); err != nil {
+	if err := permute(); err != nil {
 		return nil, fmt.Errorf("%v %v %w\nState: %v", overload.Decl.Source, overload.Decl, err, state)
 	}
 
@@ -243,18 +247,18 @@
 	return sb.String()
 }
 
-func (s *permutationState) permutateFQN(in sem.FullyQualifiedName) ([]sem.FullyQualifiedName, error) {
+func (s *permutationState) permuteFQN(in sem.FullyQualifiedName) ([]sem.FullyQualifiedName, error) {
 	args := append([]any{}, in.TemplateArguments...)
 	out := []sem.FullyQualifiedName{}
 
-	// permutate appends a permutation to out.
-	// permutate may be chained to generate N-dimensional permutations.
-	var permutate func() error
+	// permute appends a permutation to out.
+	// permute may be chained to generate N-dimensional permutations.
+	var permute func() error
 
 	switch target := in.Target.(type) {
 	case *sem.Type:
-		permutate = func() error {
-			// Inner-most permutate lambda.
+		permute = func() error {
+			// Inner-most permute lambda.
 			// Append a the current permutation to out
 			out = append(out, sem.FullyQualifiedName{Target: in.Target, TemplateArguments: args})
 			// Clone the argument list, for the next permutation to use.
@@ -263,7 +267,7 @@
 		}
 	case sem.TemplateParam:
 		if ty, ok := s.templateTypes[target]; ok {
-			permutate = func() error {
+			permute = func() error {
 				out = append(out, ty)
 				return nil
 			}
@@ -271,14 +275,14 @@
 			return nil, fmt.Errorf("'%v' was not found in templateTypes", target.GetName())
 		}
 	case *sem.TypeMatcher:
-		permutate = func() error {
+		permute = func() error {
 			for _, ty := range target.Types {
 				out = append(out, sem.FullyQualifiedName{Target: ty})
 			}
 			return nil
 		}
 	case *sem.EnumMatcher:
-		permutate = func() error {
+		permute = func() error {
 			for _, o := range target.Options {
 				if !o.IsInternal {
 					out = append(out, sem.FullyQualifiedName{Target: o})
@@ -287,7 +291,7 @@
 			return nil
 		}
 	case *sem.Enum:
-		permutate = func() error {
+		permute = func() error {
 			for _, e := range target.Entries {
 				if !e.IsInternal {
 					out = append(out, sem.FullyQualifiedName{Target: e})
@@ -300,8 +304,8 @@
 	}
 
 	for i, arg := range in.TemplateArguments {
-		i := i            // Capture iterator value for anonymous functions
-		next := permutate // Permutation chaining
+		i := i          // Capture iterator value for anonymous functions
+		next := permute // Permutation chaining
 		switch arg := arg.(type) {
 		case sem.FullyQualifiedName:
 			switch target := arg.Target.(type) {
@@ -314,14 +318,14 @@
 					return nil, fmt.Errorf("'%v' was not found in templateTypes or templateNumbers", target.GetName())
 				}
 			default:
-				perms, err := s.permutateFQN(arg)
+				perms, err := s.permuteFQN(arg)
 				if err != nil {
 					return nil, fmt.Errorf("while processing template argument %v: %v", i, err)
 				}
 				if len(perms) == 0 {
 					return nil, fmt.Errorf("template argument %v has no permutations", i)
 				}
-				permutate = func() error {
+				permute = func() error {
 					for _, f := range perms {
 						args[i] = f
 						if err := next(); err != nil {
@@ -332,11 +336,11 @@
 				}
 			}
 		default:
-			return nil, fmt.Errorf("permutateFQN() unhandled template argument type: %T", arg)
+			return nil, fmt.Errorf("permuteFQN() unhandled template argument type: %T", arg)
 		}
 	}
 
-	if err := permutate(); err != nil {
+	if err := permute(); err != nil {
 		return nil, fmt.Errorf("while processing fully qualified name '%v': %w", in.Target.GetName(), err)
 	}
 
diff --git a/tools/src/tint/intrinsic/parser/parser.go b/tools/src/tint/intrinsic/parser/parser.go
index cacbb13..86379d4 100644
--- a/tools/src/tint/intrinsic/parser/parser.go
+++ b/tools/src/tint/intrinsic/parser/parser.go
@@ -368,8 +368,8 @@
 	return m
 }
 
-func (p *parser) templateParams() ast.TemplateParams {
-	t := ast.TemplateParams{}
+func (p *parser) templateParams() []ast.TemplateParam {
+	t := []ast.TemplateParam{}
 	p.expect(tok.Lt, "template parameter list")
 	for p.err == nil && p.peekIs(0, tok.Identifier) {
 		t = append(t, p.templateParam())
diff --git a/tools/src/tint/intrinsic/parser/parser_test.go b/tools/src/tint/intrinsic/parser/parser_test.go
index 93a9fc1..76ed5cd 100644
--- a/tools/src/tint/intrinsic/parser/parser_test.go
+++ b/tools/src/tint/intrinsic/parser/parser_test.go
@@ -85,7 +85,7 @@
 			ast.AST{
 				Types: []ast.TypeDecl{{
 					Name: "T",
-					TemplateParams: ast.TemplateParams{
+					TemplateParams: []ast.TemplateParam{
 						{Name: "A"},
 						{Name: "B"},
 						{Name: "C"},
@@ -256,7 +256,7 @@
 				Builtins: []ast.IntrinsicDecl{{
 					Kind: ast.Builtin,
 					Name: "F",
-					TemplateParams: ast.TemplateParams{
+					TemplateParams: []ast.TemplateParam{
 						{
 							Name: "A", Type: ast.TemplatedName{
 								Name: "B",
@@ -276,7 +276,7 @@
 				Builtins: []ast.IntrinsicDecl{{
 					Kind: ast.Builtin,
 					Name: "F",
-					TemplateParams: ast.TemplateParams{
+					TemplateParams: []ast.TemplateParam{
 						{Name: "T"},
 					},
 					Parameters: ast.Parameters{
@@ -394,7 +394,7 @@
 				Operators: []ast.IntrinsicDecl{{
 					Kind: ast.Operator,
 					Name: "F",
-					TemplateParams: ast.TemplateParams{
+					TemplateParams: []ast.TemplateParam{
 						{
 							Name: "A", Type: ast.TemplatedName{
 								Name: "B",
@@ -414,7 +414,7 @@
 				Operators: []ast.IntrinsicDecl{{
 					Kind: ast.Operator,
 					Name: "F",
-					TemplateParams: ast.TemplateParams{
+					TemplateParams: []ast.TemplateParam{
 						{Name: "T"},
 					},
 					Parameters: ast.Parameters{
@@ -518,7 +518,7 @@
 				Constructors: []ast.IntrinsicDecl{{
 					Kind: ast.Constructor,
 					Name: "F",
-					TemplateParams: ast.TemplateParams{
+					TemplateParams: []ast.TemplateParam{
 						{
 							Name: "A", Type: ast.TemplatedName{
 								Name: "B",
@@ -538,7 +538,7 @@
 				Constructors: []ast.IntrinsicDecl{{
 					Kind: ast.Constructor,
 					Name: "F",
-					TemplateParams: ast.TemplateParams{
+					TemplateParams: []ast.TemplateParam{
 						{Name: "T"},
 					},
 					Parameters: ast.Parameters{
@@ -642,7 +642,7 @@
 				Converters: []ast.IntrinsicDecl{{
 					Kind: ast.Converter,
 					Name: "F",
-					TemplateParams: ast.TemplateParams{
+					TemplateParams: []ast.TemplateParam{
 						{
 							Name: "A", Type: ast.TemplatedName{
 								Name: "B",
@@ -662,7 +662,7 @@
 				Converters: []ast.IntrinsicDecl{{
 					Kind: ast.Converter,
 					Name: "F",
-					TemplateParams: ast.TemplateParams{
+					TemplateParams: []ast.TemplateParam{
 						{Name: "T"},
 					},
 					Parameters: ast.Parameters{
diff --git a/tools/src/tint/intrinsic/resolver/resolve.go b/tools/src/tint/intrinsic/resolver/resolve.go
index 3cf5ece..63e14df 100644
--- a/tools/src/tint/intrinsic/resolver/resolve.go
+++ b/tools/src/tint/intrinsic/resolver/resolve.go
@@ -320,19 +320,19 @@
 	// Create a new scope for resolving template parameters
 	s := newScope(&r.globals)
 
+	// Construct the semantic overload
+	overload := &sem.Overload{
+		Decl:       a,
+		Intrinsic:  intrinsic,
+		Parameters: make([]sem.Parameter, len(a.Parameters)),
+	}
+
 	// Resolve the declared template parameters
-	templateParams, err := r.templateParams(&s, a.TemplateParams)
+	templates, err := r.templateParams(&s, a.TemplateParams)
 	if err != nil {
 		return err
 	}
-
-	// Construct the semantic overload
-	overload := &sem.Overload{
-		Decl:           a,
-		Intrinsic:      intrinsic,
-		Parameters:     make([]sem.Parameter, len(a.Parameters)),
-		TemplateParams: templateParams,
-	}
+	overload.Templates = templates
 
 	// Process overload attributes
 	if stageDeco := a.Attributes.Take("stage"); stageDeco != nil {
@@ -401,23 +401,9 @@
 	// Append the overload to the intrinsic
 	intrinsic.Overloads = append(intrinsic.Overloads, overload)
 
-	// Sort the template parameters by resolved type. Append these to
-	// sem.Overload.TemplateTypes or sem.Overload.TemplateNumbers based on their kind.
-	for _, param := range templateParams {
-		switch param := param.(type) {
-		case *sem.TemplateTypeParam:
-			overload.TemplateTypes = append(overload.TemplateTypes, param)
-		case *sem.TemplateEnumParam, *sem.TemplateNumberParam:
-			overload.TemplateNumbers = append(overload.TemplateNumbers, param)
-		}
-	}
-
-	// Update high-water marks of template types and numbers
-	if r.s.MaxTemplateTypes < len(overload.TemplateTypes) {
-		r.s.MaxTemplateTypes = len(overload.TemplateTypes)
-	}
-	if r.s.MaxTemplateNumbers < len(overload.TemplateNumbers) {
-		r.s.MaxTemplateNumbers = len(overload.TemplateNumbers)
+	// Update high-water mark of templates
+	if n := len(overload.Templates); r.s.MaxTemplates < n {
+		r.s.MaxTemplates = n
 	}
 
 	// Resolve the parameters
@@ -476,9 +462,6 @@
 	if err != nil {
 		return sem.FullyQualifiedName{}, err
 	}
-	if _, ok := target.(*sem.TypeMatcher); ok {
-		return sem.FullyQualifiedName{}, fmt.Errorf("%v type matcher cannot be used directly here. Use a matcher constrained template argument", arg.Source)
-	}
 	fqn := sem.FullyQualifiedName{
 		Target:            target,
 		TemplateArguments: make([]interface{}, len(arg.TemplateArgs)),
@@ -493,12 +476,12 @@
 	return fqn, nil
 }
 
-// templateParams() resolves the ast.TemplateParams into list of sem.TemplateParam.
+// templateParams() resolves the ast.TemplateParams list into a sem.TemplateParam list.
 // Each sem.TemplateParam is registered with the scope s.
-func (r *resolver) templateParams(s *scope, l ast.TemplateParams) ([]sem.TemplateParam, error) {
-	out := []sem.TemplateParam{}
+func (r *resolver) templateParams(s *scope, l []ast.TemplateParam) ([]sem.TemplateParam, error) {
+	out := make([]sem.TemplateParam, 0, len(l))
 	for _, ast := range l {
-		param, err := r.templateParam(ast)
+		param, err := r.templateParam(s, ast)
 		if err != nil {
 			return nil, err
 		}
@@ -510,25 +493,25 @@
 
 // templateParams() resolves the ast.TemplateParam into sem.TemplateParam, which
 // is either a sem.TemplateEnumParam or a sem.TemplateTypeParam.
-func (r *resolver) templateParam(a ast.TemplateParam) (sem.TemplateParam, error) {
+func (r *resolver) templateParam(s *scope, a ast.TemplateParam) (sem.TemplateParam, error) {
 	if a.Type.Name == "num" {
-		return &sem.TemplateNumberParam{Name: a.Name}, nil
+		return &sem.TemplateNumberParam{Name: a.Name, ASTParam: a}, nil
 	}
 
 	if a.Type.Name != "" {
-		resolved, err := r.lookupNamed(&r.globals, a.Type)
+		resolved, err := r.fullyQualifiedName(s, a.Type)
 		if err != nil {
 			return nil, err
 		}
-		switch r := resolved.(type) {
+		switch r := resolved.Target.(type) {
 		case *sem.Enum:
-			return &sem.TemplateEnumParam{Name: a.Name, Enum: r}, nil
+			return &sem.TemplateEnumParam{Name: a.Name, ASTParam: a, Enum: r}, nil
 		case *sem.EnumMatcher:
-			return &sem.TemplateEnumParam{Name: a.Name, Enum: r.Enum, Matcher: r}, nil
+			return &sem.TemplateEnumParam{Name: a.Name, ASTParam: a, Enum: r.Enum, Matcher: r}, nil
 		case *sem.TypeMatcher:
-			return &sem.TemplateTypeParam{Name: a.Name, Type: r}, nil
+			return &sem.TemplateTypeParam{Name: a.Name, ASTParam: a, Type: &resolved}, nil
 		case *sem.Type:
-			return &sem.TemplateTypeParam{Name: a.Name, Type: r}, nil
+			return &sem.TemplateTypeParam{Name: a.Name, ASTParam: a, Type: &resolved}, nil
 		default:
 			return nil, fmt.Errorf("%v invalid template parameter type '%v'", a.Source, a.Type.Name)
 		}
@@ -708,7 +691,7 @@
 		switch n := n.(type) {
 		case *sem.TemplateTypeParam:
 			if n.Type != nil {
-				return n.Type
+				return n.Type.Target.(sem.ResolvableType)
 			}
 			return anyType
 		case *sem.Type:
diff --git a/tools/src/tint/intrinsic/resolver/resolver_test.go b/tools/src/tint/intrinsic/resolver/resolver_test.go
index 273a09a..294691a 100644
--- a/tools/src/tint/intrinsic/resolver/resolver_test.go
+++ b/tools/src/tint/intrinsic/resolver/resolver_test.go
@@ -196,6 +196,20 @@
 @must_use fn f() -> f32`,
 			success,
 		}, {
+			`
+type f32
+type P<T>
+match m: f32
+fn f(m)`,
+			success,
+		}, {
+			`
+type f32
+type P<T>
+match m: f32
+fn f(P<m>)`,
+			success,
+		}, {
 			`enum E {A A}`,
 			`
 file.txt:1:11 duplicate enum entry 'A'
@@ -524,20 +538,6 @@
 			`file.txt:4:16 cannot use template enum 'E' as template number`,
 		}, {
 			`
-type f32
-type P<T>
-match m: f32
-fn f(m)`,
-			`file.txt:4:6 type matcher cannot be used directly here. Use a matcher constrained template argument`,
-		}, {
-			`
-type f32
-type P<T>
-match m: f32
-fn f(P<m>)`,
-			`file.txt:4:8 type matcher cannot be used directly here. Use a matcher constrained template argument`,
-		}, {
-			`
 @must_use fn f()`,
 			`file.txt:1:2 @must_use can only be used on a function with a return type`,
 		},
diff --git a/tools/src/tint/intrinsic/sem/sem.go b/tools/src/tint/intrinsic/sem/sem.go
index e440e01..4b9d8f3 100644
--- a/tools/src/tint/intrinsic/sem/sem.go
+++ b/tools/src/tint/intrinsic/sem/sem.go
@@ -46,9 +46,7 @@
 	BinaryOperators           []*Intrinsic
 	ConstructorsAndConverters []*Intrinsic
 	// Maximum number of template types used across all builtins
-	MaxTemplateTypes int
-	// Maximum number of template numbers used across all builtins
-	MaxTemplateNumbers int
+	MaxTemplates int
 	// The alphabetically sorted list of unique parameter names
 	UniqueParameterNames []string
 }
@@ -161,22 +159,68 @@
 	Options        []*EnumEntry
 }
 
-// TemplateEnumParam is a template enum parameter
-type TemplateEnumParam struct {
-	Name    string
-	Enum    *Enum
-	Matcher *EnumMatcher // Optional
-}
+// TemplateKind is an enumerator of template kinds.
+type TemplateKind string
+
+const (
+	// A templated type
+	TypeTemplate TemplateKind = "Type"
+	// A templated number
+	NumberTemplate TemplateKind = "Number"
+)
 
 // TemplateTypeParam is a template type parameter
 type TemplateTypeParam struct {
-	Name string
-	Type ResolvableType
+	Name     string
+	ASTParam ast.TemplateParam
+	Type     *FullyQualifiedName
+}
+
+func (t *TemplateTypeParam) AST() ast.TemplateParam     { return t.ASTParam }
+func (t *TemplateTypeParam) TemplateKind() TemplateKind { return TypeTemplate }
+
+// Format implements the fmt.Formatter interface
+func (t *TemplateTypeParam) Format(w fmt.State, verb rune) {
+	fmt.Fprint(w, t.Name)
+	if t.Type != nil {
+		fmt.Fprint(w, ": ")
+		fmt.Fprint(w, *t.Type)
+	}
+}
+
+// TemplateEnumParam is a template enum parameter
+type TemplateEnumParam struct {
+	Name     string
+	ASTParam ast.TemplateParam
+	Enum     *Enum
+	Matcher  *EnumMatcher // Optional
+}
+
+func (t *TemplateEnumParam) AST() ast.TemplateParam     { return t.ASTParam }
+func (t *TemplateEnumParam) TemplateKind() TemplateKind { return NumberTemplate }
+
+// Format implements the fmt.Formatter interface
+func (t *TemplateEnumParam) Format(w fmt.State, verb rune) {
+	fmt.Fprint(w, t.Name)
+	if t.Enum != nil {
+		fmt.Fprint(w, ": ")
+		fmt.Fprint(w, *t.Enum)
+	}
 }
 
 // TemplateNumberParam is a template type parameter
 type TemplateNumberParam struct {
-	Name string
+	Name     string
+	ASTParam ast.TemplateParam
+}
+
+func (t *TemplateNumberParam) AST() ast.TemplateParam     { return t.ASTParam }
+func (t *TemplateNumberParam) TemplateKind() TemplateKind { return NumberTemplate }
+
+// Format implements the fmt.Formatter interface
+func (t *TemplateNumberParam) Format(w fmt.State, verb rune) {
+	fmt.Fprint(w, t.Name)
+	fmt.Fprint(w, ": num")
 }
 
 // Intrinsic describes the overloads of a builtin or operator
@@ -185,13 +229,36 @@
 	Overloads []*Overload
 }
 
+// IntrinsicTemplates is a list of TemplateParam, used by an Intrinsic.
+type IntrinsicTemplates []TemplateParam
+
+// Types() returns all the template type parameters
+func (t IntrinsicTemplates) Types() []*TemplateTypeParam {
+	out := []*TemplateTypeParam{}
+	for _, p := range t {
+		if ty, ok := p.(*TemplateTypeParam); ok {
+			out = append(out, ty)
+		}
+	}
+	return out
+}
+
+// Numbers() returns all the template number parameters.
+func (t IntrinsicTemplates) Numbers() []TemplateParam {
+	out := []TemplateParam{}
+	for _, p := range t {
+		if _, ok := p.(*TemplateTypeParam); !ok {
+			out = append(out, p)
+		}
+	}
+	return out
+}
+
 // Overload describes a single overload of a builtin or operator
 type Overload struct {
 	Decl              ast.IntrinsicDecl
 	Intrinsic         *Intrinsic
-	TemplateParams    []TemplateParam
-	TemplateTypes     []*TemplateTypeParam
-	TemplateNumbers   []TemplateParam
+	Templates         IntrinsicTemplates
 	ReturnType        *FullyQualifiedName
 	Parameters        []Parameter
 	CanBeUsedInStage  StageUses
@@ -231,14 +298,9 @@
 		fmt.Fprintf(w, "op ")
 	}
 	fmt.Fprintf(w, "%v", o.Intrinsic.Name)
-	if len(o.TemplateParams) > 0 {
+	if len(o.Templates) > 0 {
 		fmt.Fprintf(w, "<")
-		for i, t := range o.TemplateParams {
-			if i > 0 {
-				fmt.Fprint(w, ", ")
-			}
-			fmt.Fprintf(w, "%v", t)
-		}
+		formatList(w, o.Templates)
 		fmt.Fprintf(w, ">")
 	}
 	fmt.Fprint(w, "(")
@@ -284,12 +346,7 @@
 	fmt.Fprint(w, f.Target.GetName())
 	if len(f.TemplateArguments) > 0 {
 		fmt.Fprintf(w, "<")
-		for i, t := range f.TemplateArguments {
-			if i > 0 {
-				fmt.Fprint(w, ", ")
-			}
-			fmt.Fprintf(w, "%v", t)
-		}
+		formatList(w, f.TemplateArguments)
 		fmt.Fprintf(w, ">")
 	}
 }
@@ -297,13 +354,10 @@
 // TemplateParam is a TemplateEnumParam, TemplateTypeParam or TemplateNumberParam
 type TemplateParam interface {
 	Named
-	isTemplateParam()
+	TemplateKind() TemplateKind
+	AST() ast.TemplateParam
 }
 
-func (*TemplateEnumParam) isTemplateParam()   {}
-func (*TemplateTypeParam) isTemplateParam()   {}
-func (*TemplateNumberParam) isTemplateParam() {}
-
 // ResolvableType is a Type, TypeMatcher or TemplateTypeParam
 type ResolvableType interface {
 	Named
@@ -352,3 +406,13 @@
 
 // GetName returns the name of the TemplateNumberParam
 func (t *TemplateNumberParam) GetName() string { return t.Name }
+
+// formatList writes the comma separated list to w.
+func formatList[T any](w fmt.State, list []T) {
+	for i, v := range list {
+		if i > 0 {
+			fmt.Fprint(w, ", ")
+		}
+		fmt.Fprintf(w, "%v", v)
+	}
+}