tint: Add operator support to intrinsic-gen

Adapt the builtin parsing and resolving to also support operators.
Will be used to generate intrinsic table entries for operators.

This will simplify maintenance of the operators, and will greatly
simplify the [AbstractInt -> i32|u32] [AbstractFloat -> f32|f16] logic.

Bug: tint:1504
Change-Id: Id75735ea24e501877418812185796f3fba88a521
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/89026
Commit-Queue: Ben Clayton <bclayton@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
diff --git a/tools/src/cmd/intrinsic-gen/ast/ast.go b/tools/src/cmd/intrinsic-gen/ast/ast.go
index 92dcaa7..17454a0 100644
--- a/tools/src/cmd/intrinsic-gen/ast/ast.go
+++ b/tools/src/cmd/intrinsic-gen/ast/ast.go
@@ -28,7 +28,8 @@
 	Enums     []EnumDecl
 	Types     []TypeDecl
 	Matchers  []MatcherDecl
-	Functions []FunctionDecl
+	Builtins  []IntrinsicDecl
+	Operators []IntrinsicDecl
 }
 
 func (a AST) String() string {
@@ -45,8 +46,12 @@
 		fmt.Fprintf(&sb, "%v", m)
 		fmt.Fprintln(&sb)
 	}
-	for _, f := range a.Functions {
-		fmt.Fprintf(&sb, "%v", f)
+	for _, b := range a.Builtins {
+		fmt.Fprintf(&sb, "%v", b)
+		fmt.Fprintln(&sb)
+	}
+	for _, o := range a.Operators {
+		fmt.Fprintf(&sb, "%v", o)
 		fmt.Fprintln(&sb)
 	}
 	return sb.String()
@@ -98,9 +103,18 @@
 	m.Options.Format(w, verb)
 }
 
-// FunctionDecl describes a function declaration
-type FunctionDecl struct {
+// IntrinsicKind is either a Builtin or Operator
+type IntrinsicKind string
+
+const (
+	Builtin  IntrinsicKind = "builtin"
+	Operator IntrinsicKind = "operator"
+)
+
+// IntrinsicDecl describes a builtin or operator declaration
+type IntrinsicDecl struct {
 	Source         tok.Source
+	Kind           IntrinsicKind
 	Name           string
 	Decorations    Decorations
 	TemplateParams TemplateParams
@@ -109,13 +123,19 @@
 }
 
 // Format implements the fmt.Formatter interface
-func (f FunctionDecl) Format(w fmt.State, verb rune) {
-	fmt.Fprintf(w, "fn %v", f.Name)
-	f.TemplateParams.Format(w, verb)
-	f.Parameters.Format(w, verb)
-	if f.ReturnType != nil {
+func (i IntrinsicDecl) Format(w fmt.State, verb rune) {
+	switch i.Kind {
+	case Builtin:
+		fmt.Fprintf(w, "fn ")
+	case Operator:
+		fmt.Fprintf(w, "op ")
+	}
+	fmt.Fprintf(w, "%v", i.Name)
+	i.TemplateParams.Format(w, verb)
+	i.Parameters.Format(w, verb)
+	if i.ReturnType != nil {
 		fmt.Fprintf(w, " -> ")
-		f.ReturnType.Format(w, verb)
+		i.ReturnType.Format(w, verb)
 	}
 }
 
diff --git a/tools/src/cmd/intrinsic-gen/gen/builtin_table.go b/tools/src/cmd/intrinsic-gen/gen/builtin_table.go
index b3bfa16..94c8a5c 100644
--- a/tools/src/cmd/intrinsic-gen/gen/builtin_table.go
+++ b/tools/src/cmd/intrinsic-gen/gen/builtin_table.go
@@ -22,8 +22,8 @@
 	"dawn.googlesource.com/dawn/tools/src/lut"
 )
 
-// BuiltinTable holds data specific to the intrinsic_table.inl.tmpl template
-type BuiltinTable struct {
+// IntrinsicTable holds data specific to the intrinsic_table.inl.tmpl template
+type IntrinsicTable struct {
 	// The semantic info
 	Sem *sem.Sem
 
@@ -42,7 +42,8 @@
 	OpenNumbers    []OpenNumber // kOpenNumbers table content
 	Parameters     []Parameter  // kParameters table content
 	Overloads      []Overload   // kOverloads table content
-	Functions      []Function   // kBuiltins table content
+	Builtins       []Intrinsic  // kBuiltins table content
+	Operators      []Intrinsic  // kOperators table content
 }
 
 // OpenType is used to create the C++ OpenTypeInfo structure
@@ -68,9 +69,9 @@
 	// The parameter usage (parameter name)
 	Usage string
 
-	// Index into BuiltinTable.MatcherIndices, beginning the list of matchers
+	// Index into IntrinsicTable.MatcherIndices, beginning the list of matchers
 	// required to match the parameter type. The matcher indices index
-	// into BuiltinTable::TMatchers and / or BuiltinTable::NMatchers.
+	// into IntrinsicTable::TMatchers and / or IntrinsicTable::NMatchers.
 	// These indices are consumed by the matchers themselves.
 	// The first index is always a TypeMatcher.
 	MatcherIndicesOffset *int
@@ -84,15 +85,15 @@
 	NumOpenTypes int
 	// Total number of open numbers for the overload
 	NumOpenNumbers int
-	// Index to the first open type in BuiltinTable.OpenTypes
+	// Index to the first open type in IntrinsicTable.OpenTypes
 	OpenTypesOffset *int
-	// Index to the first open number in BuiltinTable.OpenNumbers
+	// Index to the first open number in IntrinsicTable.OpenNumbers
 	OpenNumbersOffset *int
-	// Index to the first parameter in BuiltinTable.Parameters
+	// Index to the first parameter in IntrinsicTable.Parameters
 	ParametersOffset *int
-	// Index into BuiltinTable.MatcherIndices, 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 BuiltinTable::TMatchers and / or BuiltinTable::NMatchers.
+	// into IntrinsicTable::TMatchers and / or IntrinsicTable::NMatchers.
 	// These indices are consumed by the matchers themselves.
 	// The first index is always a TypeMatcher.
 	ReturnMatcherIndicesOffset *int
@@ -102,17 +103,18 @@
 	IsDeprecated bool
 }
 
-// Function is used to create the C++ IntrinsicInfo structure
-type Function struct {
+// Intrinsic is used to create the C++ IntrinsicInfo structure
+type Intrinsic struct {
+	Name                 string
 	OverloadDescriptions []string
 	NumOverloads         int
 	OverloadsOffset      *int
 }
 
-// Helper for building the BuiltinTable
-type BuiltinTableBuilder struct {
+// Helper for building the IntrinsicTable
+type IntrinsicTableBuilder struct {
 	// The output of the builder
-	BuiltinTable
+	IntrinsicTable
 
 	// Lookup tables.
 	// These are packed (compressed) once all the entries have been added.
@@ -127,7 +129,7 @@
 
 // Helper for building a single overload
 type overloadBuilder struct {
-	*BuiltinTableBuilder
+	*IntrinsicTableBuilder
 	// Maps TemplateParam to index in openTypes
 	openTypeIndex map[sem.TemplateParam]int
 	// Maps TemplateParam to index in openNumbers
@@ -138,9 +140,9 @@
 	openNumbers []OpenNumber
 	// All parameters declared by the overload
 	parameters []Parameter
-	// Index into BuiltinTable.MatcherIndices, 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 BuiltinTable::TMatchers and / or BuiltinTable::NMatchers.
+	// into IntrinsicTable::TMatchers and / or IntrinsicTable::NMatchers.
 	// These indices are consumed by the matchers themselves.
 	// The first index is always a TypeMatcher.
 	returnTypeMatcherIndicesOffset *int
@@ -148,7 +150,7 @@
 
 // layoutMatchers assigns each of the TMatchers and NMatchers a unique index
 // in the C++ Matchers::type and Matchers::number arrays, respectively.
-func (b *BuiltinTableBuilder) layoutMatchers(s *sem.Sem) {
+func (b *IntrinsicTableBuilder) layoutMatchers(s *sem.Sem) {
 	// First MaxOpenTypes of TMatchers are open types
 	b.TMatchers = make([]sem.Named, s.MaxOpenTypes)
 	for _, m := range s.Types {
@@ -169,11 +171,11 @@
 }
 
 // buildOverload constructs an Overload for a sem.Overload
-func (b *BuiltinTableBuilder) buildOverload(o *sem.Overload) (Overload, error) {
+func (b *IntrinsicTableBuilder) buildOverload(o *sem.Overload) (Overload, error) {
 	ob := overloadBuilder{
-		BuiltinTableBuilder: b,
-		openTypeIndex:       map[sem.TemplateParam]int{},
-		openNumberIndex:     map[sem.TemplateParam]int{},
+		IntrinsicTableBuilder: b,
+		openTypeIndex:         map[sem.TemplateParam]int{},
+		openNumberIndex:       map[sem.TemplateParam]int{},
 	}
 
 	if err := ob.buildOpenTypes(o); err != nil {
@@ -279,7 +281,7 @@
 }
 
 // matcherIndex returns the index of TMatcher or NMatcher in
-// BuiltinTable.TMatcher or BuiltinTable.NMatcher, respectively.
+// IntrinsicTable.TMatcher or IntrinsicTable.NMatcher, respectively.
 func (b *overloadBuilder) matcherIndex(n sem.Named) (int, error) {
 	switch n := n.(type) {
 	case *sem.Type, *sem.TypeMatcher:
@@ -342,10 +344,10 @@
 	return out, nil
 }
 
-// buildBuiltinTable builds the BuiltinTable from the semantic info
-func buildBuiltinTable(s *sem.Sem) (*BuiltinTable, error) {
-	b := BuiltinTableBuilder{
-		BuiltinTable: BuiltinTable{
+// buildIntrinsicTable builds the IntrinsicTable from the semantic info
+func buildIntrinsicTable(s *sem.Sem) (*IntrinsicTable, error) {
+	b := IntrinsicTableBuilder{
+		IntrinsicTable: IntrinsicTable{
 			Sem:           s,
 			TMatcherIndex: map[sem.Named]int{},
 			NMatcherIndex: map[sem.Named]int{},
@@ -359,22 +361,34 @@
 
 	b.layoutMatchers(s)
 
-	for _, f := range s.Functions {
-		overloads := make([]Overload, len(f.Overloads))
-		overloadDescriptions := make([]string, len(f.Overloads))
-		for i, o := range f.Overloads {
-			overloadDescriptions[i] = fmt.Sprint(o.Decl)
-			var err error
-			if overloads[i], err = b.buildOverload(o); err != nil {
-				return nil, err
+	buildIntrinsics := func(in []*sem.Intrinsic) ([]Intrinsic, error) {
+		out := make([]Intrinsic, len(in))
+		for i, f := range in {
+			overloads := make([]Overload, len(f.Overloads))
+			overloadDescriptions := make([]string, len(f.Overloads))
+			for i, o := range f.Overloads {
+				overloadDescriptions[i] = fmt.Sprint(o.Decl)
+				var err error
+				if overloads[i], err = b.buildOverload(o); err != nil {
+					return nil, err
+				}
+			}
+			out[i] = Intrinsic{
+				Name:                 f.Name,
+				OverloadDescriptions: overloadDescriptions,
+				NumOverloads:         len(overloads),
+				OverloadsOffset:      b.lut.overloads.Add(overloads),
 			}
 		}
+		return out, nil
+	}
 
-		b.Functions = append(b.Functions, Function{
-			OverloadDescriptions: overloadDescriptions,
-			NumOverloads:         len(overloads),
-			OverloadsOffset:      b.lut.overloads.Add(overloads),
-		})
+	var err error
+	if b.Builtins, err = buildIntrinsics(s.Builtins); err != nil {
+		return nil, err
+	}
+	if b.Operators, err = buildIntrinsics(s.Operators); err != nil {
+		return nil, err
 	}
 
 	b.lut.matcherIndices.Compact()
@@ -383,5 +397,5 @@
 	b.lut.parameters.Compact()
 	b.lut.overloads.Compact()
 
-	return &b.BuiltinTable, nil
+	return &b.IntrinsicTable, nil
 }
diff --git a/tools/src/cmd/intrinsic-gen/gen/generate.go b/tools/src/cmd/intrinsic-gen/gen/generate.go
index 246fb7b..5d83b19 100644
--- a/tools/src/cmd/intrinsic-gen/gen/generate.go
+++ b/tools/src/cmd/intrinsic-gen/gen/generate.go
@@ -29,8 +29,8 @@
 	s      *sem.Sem
 	t      *template.Template
 	cached struct {
-		builtinTable *BuiltinTable // lazily built by builtinTable()
-		permuter     *Permuter     // lazily built by permute()
+		intrinsicTable *IntrinsicTable // lazily built by intrinsicTable()
+		permuter       *Permuter       // lazily built by permute()
 	}
 }
 
@@ -73,7 +73,7 @@
 		"IsDeclarable":          isDeclarable,
 		"IsFirstIn":             isFirstIn,
 		"IsLastIn":              isLastIn,
-		"BuiltinTable":          g.builtinTable,
+		"IntrinsicTable":        g.intrinsicTable,
 		"Permute":               g.permute,
 		"Eval":                  g.eval,
 		"WriteFile":             func(relpath, content string) (string, error) { return "", writeFile(relpath, content) },
@@ -121,17 +121,17 @@
 	return sb.String(), nil
 }
 
-// builtinTable lazily calls and returns the result of buildBuiltinTable(),
+// intrinsicTable lazily calls and returns the result of buildIntrinsicTable(),
 // caching the result for repeated calls.
-func (g *generator) builtinTable() (*BuiltinTable, error) {
-	if g.cached.builtinTable == nil {
+func (g *generator) intrinsicTable() (*IntrinsicTable, error) {
+	if g.cached.intrinsicTable == nil {
 		var err error
-		g.cached.builtinTable, err = buildBuiltinTable(g.s)
+		g.cached.intrinsicTable, err = buildIntrinsicTable(g.s)
 		if err != nil {
 			return nil, err
 		}
 	}
-	return g.cached.builtinTable, nil
+	return g.cached.intrinsicTable, nil
 }
 
 // permute lazily calls buildPermuter(), caching the result for repeated
diff --git a/tools/src/cmd/intrinsic-gen/gen/permutate.go b/tools/src/cmd/intrinsic-gen/gen/permutate.go
index 934e966..41e6472 100644
--- a/tools/src/cmd/intrinsic-gen/gen/permutate.go
+++ b/tools/src/cmd/intrinsic-gen/gen/permutate.go
@@ -74,7 +74,7 @@
 	permutate := func() error {
 		o := sem.Overload{
 			Decl:             overload.Decl,
-			Function:         overload.Function,
+			Intrinsic:        overload.Intrinsic,
 			CanBeUsedInStage: overload.CanBeUsedInStage,
 		}
 		for i, p := range overload.Parameters {
diff --git a/tools/src/cmd/intrinsic-gen/lexer/lexer.go b/tools/src/cmd/intrinsic-gen/lexer/lexer.go
index 749ab0d..35cd73e 100644
--- a/tools/src/cmd/intrinsic-gen/lexer/lexer.go
+++ b/tools/src/cmd/intrinsic-gen/lexer/lexer.go
@@ -52,10 +52,6 @@
 			l.next()
 		case '\n':
 			l.next()
-		case '<':
-			l.tok(1, tok.Lt)
-		case '>':
-			l.tok(1, tok.Gt)
 		case '(':
 			l.tok(1, tok.Lparen)
 		case ')':
@@ -68,8 +64,14 @@
 			l.tok(1, tok.Colon)
 		case ',':
 			l.tok(1, tok.Comma)
-		case '|':
-			l.tok(1, tok.Or)
+		case '*':
+			l.tok(1, tok.Star)
+		case '+':
+			l.tok(1, tok.Plus)
+		case '%':
+			l.tok(1, tok.Modulo)
+		case '^':
+			l.tok(1, tok.Xor)
 		case '"':
 			start := l.loc
 			l.next() // Skip opening quote
@@ -81,13 +83,16 @@
 			l.next() // Skip closing quote
 		default:
 			switch {
-			case l.peek(1) == '/':
+			case l.peek(0) == '/' && l.peek(1) == '/':
 				l.skip(l.count(toFirst('\n')))
 				l.next() // Consume newline
+			case l.match("/", tok.Divide):
 			case l.match("[[", tok.Ldeco):
 			case l.match("]]", tok.Rdeco):
 			case l.match("->", tok.Arrow):
+			case l.match("-", tok.Minus):
 			case l.match("fn", tok.Function):
+			case l.match("op", tok.Operator):
 			case l.match("enum", tok.Enum):
 			case l.match("type", tok.Type):
 			case l.match("match", tok.Match):
@@ -95,6 +100,19 @@
 				l.tok(l.count(alphaNumericOrUnderscore), tok.Identifier)
 			case unicode.IsNumber(l.peek(0)):
 				l.tok(l.count(unicode.IsNumber), tok.Integer)
+			case l.match("&&", tok.AndAnd):
+			case l.match("&", tok.And):
+			case l.match("||", tok.OrOr):
+			case l.match("|", tok.Or):
+			case l.match("!=", tok.NotEqual):
+			case l.match("==", tok.Equal):
+			case l.match("=", tok.Assign):
+			case l.match("<<", tok.Shl):
+			case l.match("<=", tok.Le):
+			case l.match("<", tok.Lt):
+			case l.match(">=", tok.Ge):
+			case l.match(">>", tok.Shr):
+			case l.match(">", tok.Gt):
 			default:
 				return fmt.Errorf("%v: unexpected '%v'", l.loc, string(l.runes[0]))
 			}
diff --git a/tools/src/cmd/intrinsic-gen/lexer/lexer_test.go b/tools/src/cmd/intrinsic-gen/lexer/lexer_test.go
index 51130e5..bfc8df8 100644
--- a/tools/src/cmd/intrinsic-gen/lexer/lexer_test.go
+++ b/tools/src/cmd/intrinsic-gen/lexer/lexer_test.go
@@ -52,6 +52,9 @@
 		{"fn", tok.Token{Kind: tok.Function, Runes: []rune("fn"), Source: tok.Source{
 			S: loc(1, 1, 0), E: loc(1, 3, 2),
 		}}},
+		{"op", tok.Token{Kind: tok.Operator, Runes: []rune("op"), Source: tok.Source{
+			S: loc(1, 1, 0), E: loc(1, 3, 2),
+		}}},
 		{"type", tok.Token{Kind: tok.Type, Runes: []rune("type"), Source: tok.Source{
 			S: loc(1, 1, 0), E: loc(1, 5, 4),
 		}}},
@@ -76,6 +79,45 @@
 		{"}", tok.Token{Kind: tok.Rbrace, Runes: []rune("}"), Source: tok.Source{
 			S: loc(1, 1, 0), E: loc(1, 2, 1),
 		}}},
+		{"&&", tok.Token{Kind: tok.AndAnd, Runes: []rune("&&"), Source: tok.Source{
+			S: loc(1, 1, 0), E: loc(1, 3, 2),
+		}}},
+		{"&", tok.Token{Kind: tok.And, Runes: []rune("&"), Source: tok.Source{
+			S: loc(1, 1, 0), E: loc(1, 2, 1),
+		}}},
+		{"||", tok.Token{Kind: tok.OrOr, Runes: []rune("||"), Source: tok.Source{
+			S: loc(1, 1, 0), E: loc(1, 3, 2),
+		}}},
+		{"|", tok.Token{Kind: tok.Or, Runes: []rune("|"), Source: tok.Source{
+			S: loc(1, 1, 0), E: loc(1, 2, 1),
+		}}},
+		{"!=", tok.Token{Kind: tok.NotEqual, Runes: []rune("!="), Source: tok.Source{
+			S: loc(1, 1, 0), E: loc(1, 3, 2),
+		}}},
+		{"==", tok.Token{Kind: tok.Equal, Runes: []rune("=="), Source: tok.Source{
+			S: loc(1, 1, 0), E: loc(1, 3, 2),
+		}}},
+		{"=", tok.Token{Kind: tok.Assign, Runes: []rune("="), Source: tok.Source{
+			S: loc(1, 1, 0), E: loc(1, 2, 1),
+		}}},
+		{"<<", tok.Token{Kind: tok.Shl, Runes: []rune("<<"), Source: tok.Source{
+			S: loc(1, 1, 0), E: loc(1, 3, 2),
+		}}},
+		{"<=", tok.Token{Kind: tok.Le, Runes: []rune("<="), Source: tok.Source{
+			S: loc(1, 1, 0), E: loc(1, 3, 2),
+		}}},
+		{"<", tok.Token{Kind: tok.Lt, Runes: []rune("<"), Source: tok.Source{
+			S: loc(1, 1, 0), E: loc(1, 2, 1),
+		}}},
+		{">=", tok.Token{Kind: tok.Ge, Runes: []rune(">="), Source: tok.Source{
+			S: loc(1, 1, 0), E: loc(1, 3, 2),
+		}}},
+		{">>", tok.Token{Kind: tok.Shr, Runes: []rune(">>"), Source: tok.Source{
+			S: loc(1, 1, 0), E: loc(1, 3, 2),
+		}}},
+		{">", tok.Token{Kind: tok.Gt, Runes: []rune(">"), Source: tok.Source{
+			S: loc(1, 1, 0), E: loc(1, 2, 1),
+		}}},
 		{"[[", tok.Token{Kind: tok.Ldeco, Runes: []rune("[["), Source: tok.Source{
 			S: loc(1, 1, 0), E: loc(1, 3, 2),
 		}}},
@@ -91,6 +133,9 @@
 		{"|", tok.Token{Kind: tok.Or, Runes: []rune("|"), Source: tok.Source{
 			S: loc(1, 1, 0), E: loc(1, 2, 1),
 		}}},
+		{"*", tok.Token{Kind: tok.Star, Runes: []rune("*"), Source: tok.Source{
+			S: loc(1, 1, 0), E: loc(1, 2, 1),
+		}}},
 		{"->", tok.Token{Kind: tok.Arrow, Runes: []rune("->"), Source: tok.Source{
 			S: loc(1, 1, 0), E: loc(1, 3, 2),
 		}}},
@@ -134,10 +179,14 @@
 	for _, test := range []test{
 		{" \"abc", "test.txt:1:2 unterminated string"},
 		{" \"abc\n", "test.txt:1:2 unterminated string"},
-		{"*", "test.txt:1:1: unexpected '*'"},
+		{"£", "test.txt:1:1: unexpected '£'"},
 	} {
 		got, err := lexer.Lex([]rune(test.src), "test.txt")
-		if gotErr := err.Error(); test.expect != gotErr {
+		gotErr := "<nil>"
+		if err != nil {
+			gotErr = err.Error()
+		}
+		if test.expect != gotErr {
 			t.Errorf(`Lex() returned error "%+v", expected error "%+v"`, gotErr, test.expect)
 		}
 		if got != nil {
diff --git a/tools/src/cmd/intrinsic-gen/parser/parser.go b/tools/src/cmd/intrinsic-gen/parser/parser.go
index cd775cc..48c8a94 100644
--- a/tools/src/cmd/intrinsic-gen/parser/parser.go
+++ b/tools/src/cmd/intrinsic-gen/parser/parser.go
@@ -66,7 +66,10 @@
 			out.Types = append(out.Types, p.typeDecl(decorations))
 			decorations = nil
 		case tok.Function:
-			out.Functions = append(out.Functions, p.functionDecl(decorations))
+			out.Builtins = append(out.Builtins, p.builtinDecl(decorations))
+			decorations = nil
+		case tok.Operator:
+			out.Operators = append(out.Operators, p.operatorDecl(decorations))
 			decorations = nil
 		default:
 			p.err = fmt.Errorf("%v unexpected token '%v'", t.Source, t.Kind)
@@ -153,11 +156,12 @@
 	return out
 }
 
-func (p *parser) functionDecl(decos ast.Decorations) ast.FunctionDecl {
+func (p *parser) builtinDecl(decos ast.Decorations) ast.IntrinsicDecl {
 	p.expect(tok.Function, "function declaration")
 	name := p.expect(tok.Identifier, "function name")
-	f := ast.FunctionDecl{
+	f := ast.IntrinsicDecl{
 		Source:      name.Source,
+		Kind:        ast.Builtin,
 		Decorations: decos,
 		Name:        string(name.Runes),
 	}
@@ -172,6 +176,25 @@
 	return f
 }
 
+func (p *parser) operatorDecl(decos ast.Decorations) ast.IntrinsicDecl {
+	p.expect(tok.Operator, "operator declaration")
+	name := p.next()
+	f := ast.IntrinsicDecl{
+		Source:      name.Source,
+		Kind:        ast.Operator,
+		Decorations: decos,
+		Name:        string(name.Runes),
+	}
+	if p.peekIs(0, tok.Lt) {
+		f.TemplateParams = p.templateParams()
+	}
+	f.Parameters = p.parameters()
+	if p.match(tok.Arrow) != nil {
+		ret := p.templatedName()
+		f.ReturnType = &ret
+	}
+	return f
+}
 func (p *parser) parameters() ast.Parameters {
 	l := ast.Parameters{}
 	p.expect(tok.Lparen, "function parameter list")
@@ -270,20 +293,6 @@
 	return string(p.expect(tok.Identifier, use).Runes)
 }
 
-// TODO(bclayton): Currently unused, but will be needed for integer bounds
-// func (p *parser) integer(use string) int {
-// 	t := p.expect(tok.Integer, use)
-// 	if t.Kind != tok.Integer {
-// 		return 0
-// 	}
-// 	i, err := strconv.Atoi(string(t.Runes))
-// 	if err != nil {
-// 		p.err = err
-// 		return 0
-// 	}
-// 	return i
-// }
-
 func (p *parser) match(kind tok.Kind) *tok.Token {
 	if p.err != nil || len(p.tokens) == 0 {
 		return nil
@@ -296,6 +305,18 @@
 	return &t
 }
 
+func (p *parser) next() *tok.Token {
+	if p.err != nil {
+		return nil
+	}
+	if len(p.tokens) == 0 {
+		p.err = fmt.Errorf("reached end of file")
+	}
+	t := p.tokens[0]
+	p.tokens = p.tokens[1:]
+	return &t
+}
+
 func (p *parser) peekIs(i int, kind tok.Kind) bool {
 	t := p.peek(i)
 	if t == nil {
diff --git a/tools/src/cmd/intrinsic-gen/parser/parser_test.go b/tools/src/cmd/intrinsic-gen/parser/parser_test.go
index ae78113..9ea76c3 100644
--- a/tools/src/cmd/intrinsic-gen/parser/parser_test.go
+++ b/tools/src/cmd/intrinsic-gen/parser/parser_test.go
@@ -19,170 +19,369 @@
 
 	"dawn.googlesource.com/dawn/tools/src/cmd/intrinsic-gen/ast"
 	"dawn.googlesource.com/dawn/tools/src/cmd/intrinsic-gen/parser"
+	"dawn.googlesource.com/dawn/tools/src/utils"
+	"github.com/google/go-cmp/cmp"
 )
 
+var ignoreSource = cmp.FilterPath(func(p cmp.Path) bool {
+	return p.Last().String() == ".Source"
+}, cmp.Ignore())
+
 func TestParser(t *testing.T) {
 	type test struct {
-		src    string
-		expect ast.AST
+		location string
+		src      string
+		expect   ast.AST
 	}
 
 	for _, test := range []test{
-		{"enum E {}", ast.AST{
-			Enums: []ast.EnumDecl{{Name: "E"}},
-		}},
-		{"enum E { A [[deco]] B C }", ast.AST{
-			Enums: []ast.EnumDecl{{
-				Name: "E",
-				Entries: []ast.EnumEntry{
-					{Name: "A"},
-					{
-						Decorations: ast.Decorations{{Name: "deco"}},
-						Name:        "B",
-					},
-					{Name: "C"},
-				},
-			}},
-		}},
-		{"type T", ast.AST{
-			Types: []ast.TypeDecl{{Name: "T"}},
-		}},
-		{"type T<A, B, C>", ast.AST{
-			Types: []ast.TypeDecl{{
-				Name: "T",
-				TemplateParams: ast.TemplateParams{
-					{Name: "A"},
-					{Name: "B"},
-					{Name: "C"},
-				},
-			}},
-		}},
-		{"[[deco]] type T", ast.AST{
-			Types: []ast.TypeDecl{{
-				Decorations: ast.Decorations{
-					{Name: "deco"},
-				},
-				Name: "T",
-			}},
-		}},
-		{`[[deco("a", "b")]] type T`, ast.AST{
-			Types: []ast.TypeDecl{{
-				Decorations: ast.Decorations{
-					{Name: "deco", Values: []string{"a", "b"}},
-				},
-				Name: "T",
-			}},
-		}},
-		{"match M : A", ast.AST{
-			Matchers: []ast.MatcherDecl{{
-				Name: "M",
-				Options: ast.MatcherOptions{
-					ast.TemplatedName{Name: "A"},
-				},
-			}},
-		}},
-		{"match M : A | B", ast.AST{
-			Matchers: []ast.MatcherDecl{{
-				Name: "M",
-				Options: ast.MatcherOptions{
-					ast.TemplatedName{Name: "A"},
-					ast.TemplatedName{Name: "B"},
-				},
-			}},
-		}},
-		{"fn F()", ast.AST{
-			Functions: []ast.FunctionDecl{{
-				Name: "F",
-			}},
-		}},
-		{"[[deco]] fn F()", ast.AST{
-			Functions: []ast.FunctionDecl{{
-				Name: "F",
-				Decorations: ast.Decorations{
-					{Name: "deco"},
-				},
-			}},
-		}},
-		{"fn F(a)", ast.AST{
-			Functions: []ast.FunctionDecl{{
-				Name: "F",
-				Parameters: ast.Parameters{
-					{Type: ast.TemplatedName{Name: "a"}},
-				},
-			}},
-		}},
-		{"fn F(a: T)", ast.AST{
-			Functions: []ast.FunctionDecl{{
-				Name: "F",
-				Parameters: ast.Parameters{
-					{Name: "a", Type: ast.TemplatedName{Name: "T"}},
-				},
-			}},
-		}},
-		{"fn F(a, b)", ast.AST{
-			Functions: []ast.FunctionDecl{{
-				Name: "F",
-				Parameters: ast.Parameters{
-					{Type: ast.TemplatedName{Name: "a"}},
-					{Type: ast.TemplatedName{Name: "b"}},
-				},
-			}},
-		}},
-		{"fn F<A : B<C>>()", ast.AST{
-			Functions: []ast.FunctionDecl{{
-				Name: "F",
-				TemplateParams: ast.TemplateParams{
-					{
-						Name: "A", Type: ast.TemplatedName{
+		{
+			utils.ThisLine(),
+			"enum E {}",
+			ast.AST{
+				Enums: []ast.EnumDecl{{Name: "E"}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"enum E { A [[deco]] B C }",
+			ast.AST{
+				Enums: []ast.EnumDecl{{
+					Name: "E",
+					Entries: []ast.EnumEntry{
+						{Name: "A"},
+						{
+							Decorations: ast.Decorations{{
+								Name:   "deco",
+								Values: []string{},
+							}},
 							Name: "B",
-							TemplateArgs: ast.TemplatedNames{
-								{Name: "C"},
+						},
+						{Name: "C"},
+					},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"type T",
+			ast.AST{
+				Types: []ast.TypeDecl{{Name: "T"}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"type T<A, B, C>",
+			ast.AST{
+				Types: []ast.TypeDecl{{
+					Name: "T",
+					TemplateParams: ast.TemplateParams{
+						{Name: "A"},
+						{Name: "B"},
+						{Name: "C"},
+					},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"[[deco]] type T",
+			ast.AST{
+				Types: []ast.TypeDecl{{
+					Decorations: ast.Decorations{
+						{Name: "deco", Values: []string{}},
+					},
+					Name: "T",
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			`[[deco("a", "b")]] type T`, ast.AST{
+				Types: []ast.TypeDecl{{
+					Decorations: ast.Decorations{
+						{Name: "deco", Values: []string{"a", "b"}},
+					},
+					Name: "T",
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"match M : A",
+			ast.AST{
+				Matchers: []ast.MatcherDecl{{
+					Name: "M",
+					Options: ast.MatcherOptions{
+						ast.TemplatedName{Name: "A"},
+					},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"match M : A | B",
+			ast.AST{
+				Matchers: []ast.MatcherDecl{{
+					Name: "M",
+					Options: ast.MatcherOptions{
+						ast.TemplatedName{Name: "A"},
+						ast.TemplatedName{Name: "B"},
+					},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"fn F()",
+			ast.AST{
+				Builtins: []ast.IntrinsicDecl{{
+					Kind:       ast.Builtin,
+					Name:       "F",
+					Parameters: ast.Parameters{},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"[[deco]] fn F()",
+			ast.AST{
+				Builtins: []ast.IntrinsicDecl{{
+					Kind: ast.Builtin,
+					Name: "F",
+					Decorations: ast.Decorations{
+						{Name: "deco", Values: []string{}},
+					},
+					Parameters: ast.Parameters{},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"fn F(a)",
+			ast.AST{
+				Builtins: []ast.IntrinsicDecl{{
+					Kind: ast.Builtin,
+					Name: "F",
+					Parameters: ast.Parameters{
+						{Type: ast.TemplatedName{Name: "a"}},
+					},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"fn F(a: T)",
+			ast.AST{
+				Builtins: []ast.IntrinsicDecl{{
+					Kind: ast.Builtin,
+					Name: "F",
+					Parameters: ast.Parameters{
+						{Name: "a", Type: ast.TemplatedName{Name: "T"}},
+					},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"fn F(a, b)",
+			ast.AST{
+				Builtins: []ast.IntrinsicDecl{{
+					Kind: ast.Builtin,
+					Name: "F",
+					Parameters: ast.Parameters{
+						{Type: ast.TemplatedName{Name: "a"}},
+						{Type: ast.TemplatedName{Name: "b"}},
+					},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"fn F<A : B<C> >()",
+			ast.AST{
+				Builtins: []ast.IntrinsicDecl{{
+					Kind: ast.Builtin,
+					Name: "F",
+					TemplateParams: ast.TemplateParams{
+						{
+							Name: "A", Type: ast.TemplatedName{
+								Name: "B",
+								TemplateArgs: ast.TemplatedNames{
+									{Name: "C"},
+								},
 							},
 						},
 					},
-				},
-			}},
-		}},
-		{"fn F<T>(a: X, b: Y<T>)", ast.AST{
-			Functions: []ast.FunctionDecl{{
-				Name: "F",
-				TemplateParams: ast.TemplateParams{
-					{Name: "T"},
-				},
-				Parameters: ast.Parameters{
-					{Name: "a", Type: ast.TemplatedName{Name: "X"}},
-					{Name: "b", Type: ast.TemplatedName{
-						Name:         "Y",
+					Parameters: ast.Parameters{},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"fn F<T>(a: X, b: Y<T>)",
+			ast.AST{
+				Builtins: []ast.IntrinsicDecl{{
+					Kind: ast.Builtin,
+					Name: "F",
+					TemplateParams: ast.TemplateParams{
+						{Name: "T"},
+					},
+					Parameters: ast.Parameters{
+						{Name: "a", Type: ast.TemplatedName{Name: "X"}},
+						{Name: "b", Type: ast.TemplatedName{
+							Name:         "Y",
+							TemplateArgs: []ast.TemplatedName{{Name: "T"}},
+						}},
+					},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"fn F() -> X",
+			ast.AST{
+				Builtins: []ast.IntrinsicDecl{{
+					Kind:       ast.Builtin,
+					Name:       "F",
+					ReturnType: &ast.TemplatedName{Name: "X"},
+					Parameters: ast.Parameters{},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"fn F() -> X<T>",
+			ast.AST{
+				Builtins: []ast.IntrinsicDecl{{
+					Kind: ast.Builtin,
+					Name: "F",
+					ReturnType: &ast.TemplatedName{
+						Name:         "X",
 						TemplateArgs: []ast.TemplatedName{{Name: "T"}},
-					}},
-				},
+					},
+					Parameters: ast.Parameters{},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"op F()",
+			ast.AST{
+				Operators: []ast.IntrinsicDecl{{
+					Kind:       ast.Operator,
+					Name:       "F",
+					Parameters: ast.Parameters{},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"[[deco]] op F()",
+			ast.AST{
+				Operators: []ast.IntrinsicDecl{{
+					Kind: ast.Operator,
+					Name: "F",
+					Decorations: ast.Decorations{
+						{Name: "deco", Values: []string{}},
+					},
+					Parameters: ast.Parameters{},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"op F(a)",
+			ast.AST{
+				Operators: []ast.IntrinsicDecl{{
+					Kind: ast.Operator,
+					Name: "F",
+					Parameters: ast.Parameters{
+						{Type: ast.TemplatedName{Name: "a"}},
+					},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"op F(a: T)",
+			ast.AST{
+				Operators: []ast.IntrinsicDecl{{
+					Kind: ast.Operator,
+					Name: "F",
+					Parameters: ast.Parameters{
+						{Name: "a", Type: ast.TemplatedName{Name: "T"}},
+					},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"op F(a, b)",
+			ast.AST{
+				Operators: []ast.IntrinsicDecl{{
+					Kind: ast.Operator,
+					Name: "F",
+					Parameters: ast.Parameters{
+						{Type: ast.TemplatedName{Name: "a"}},
+						{Type: ast.TemplatedName{Name: "b"}},
+					},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"op F<A : B<C> >()",
+			ast.AST{
+				Operators: []ast.IntrinsicDecl{{
+					Kind: ast.Operator,
+					Name: "F",
+					TemplateParams: ast.TemplateParams{
+						{
+							Name: "A", Type: ast.TemplatedName{
+								Name: "B",
+								TemplateArgs: ast.TemplatedNames{
+									{Name: "C"},
+								},
+							},
+						},
+					},
+					Parameters: ast.Parameters{},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"op F<T>(a: X, b: Y<T>)",
+			ast.AST{
+				Operators: []ast.IntrinsicDecl{{
+					Kind: ast.Operator,
+					Name: "F",
+					TemplateParams: ast.TemplateParams{
+						{Name: "T"},
+					},
+					Parameters: ast.Parameters{
+						{Name: "a", Type: ast.TemplatedName{Name: "X"}},
+						{Name: "b", Type: ast.TemplatedName{
+							Name:         "Y",
+							TemplateArgs: []ast.TemplatedName{{Name: "T"}},
+						}},
+					},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"op F() -> X",
+			ast.AST{
+				Operators: []ast.IntrinsicDecl{{
+					Kind:       ast.Operator,
+					Name:       "F",
+					ReturnType: &ast.TemplatedName{Name: "X"},
+					Parameters: ast.Parameters{},
+				}},
+			},
+		}, { ///////////////////////////////////////////////////////////////////
+			utils.ThisLine(),
+			"op F() -> X<T>",
+			ast.AST{
+				Operators: []ast.IntrinsicDecl{{
+					Kind: ast.Operator,
+					Name: "F",
+					ReturnType: &ast.TemplatedName{
+						Name:         "X",
+						TemplateArgs: []ast.TemplatedName{{Name: "T"}},
+					},
+					Parameters: ast.Parameters{},
+				}},
 			}},
-		}},
-		{"fn F() -> X", ast.AST{
-			Functions: []ast.FunctionDecl{{
-				Name:       "F",
-				ReturnType: &ast.TemplatedName{Name: "X"},
-			}},
-		}},
-		{"fn F() -> X<T>", ast.AST{
-			Functions: []ast.FunctionDecl{{
-				Name: "F",
-				ReturnType: &ast.TemplatedName{
-					Name:         "X",
-					TemplateArgs: []ast.TemplatedName{{Name: "T"}},
-				},
-			}},
-		}},
 	} {
 		got, err := parser.Parse(test.src, "file.txt")
 		if err != nil {
-			t.Errorf("While parsing:\n%s\nParse() returned error: %v", test.src, err)
+			t.Errorf("\n%v\nWhile parsing:\n%s\nParse() returned error: %v",
+				test.location, test.src, err)
 			continue
 		}
 
-		gotStr, expectStr := got.String(), test.expect.String()
-		if gotStr != expectStr {
-			t.Errorf("While parsing:\n%s\nGot:\n%s\nExpected:\n%s", test.src, gotStr, expectStr)
+		if diff := cmp.Diff(got, &test.expect, ignoreSource); diff != "" {
+			t.Errorf("\n%v\nWhile parsing:\n%s\n\n%s",
+				test.location, test.src, diff)
 		}
 	}
 }
@@ -194,10 +393,22 @@
 	}
 
 	for _, test := range []test{
-		{"+", "test.txt:1:1: unexpected '+'"},
-		{"123", "test.txt:1:1 unexpected token 'integer'"},
-		{"[[123]]", "test.txt:1:3 expected 'ident' for decoration name, got 'integer'"},
-		{"[[abc", "expected ']]' for decoration list, but reached end of file"},
+		{
+			"£",
+			"test.txt:1:1: unexpected '£'",
+		},
+		{
+			"123",
+			"test.txt:1:1 unexpected token 'integer'",
+		},
+		{
+			"[[123]]",
+			"test.txt:1:3 expected 'ident' for decoration name, got 'integer'",
+		},
+		{
+			"[[abc",
+			"expected ']]' for decoration list, but reached end of file",
+		},
 	} {
 		got, err := parser.Parse(test.src, "test.txt")
 		if gotErr := err.Error(); test.expect != gotErr {
diff --git a/tools/src/cmd/intrinsic-gen/resolver/resolve.go b/tools/src/cmd/intrinsic-gen/resolver/resolve.go
index f7f662a..3a09f38 100644
--- a/tools/src/cmd/intrinsic-gen/resolver/resolve.go
+++ b/tools/src/cmd/intrinsic-gen/resolver/resolve.go
@@ -28,7 +28,8 @@
 	s *sem.Sem
 
 	globals           scope
-	functions         map[string]*sem.Function
+	builtins          map[string]*sem.Intrinsic
+	operators         map[string]*sem.Intrinsic
 	enumEntryMatchers map[*sem.EnumEntry]*sem.EnumMatcher
 }
 
@@ -38,7 +39,8 @@
 		a:                 a,
 		s:                 sem.New(),
 		globals:           newScope(nil),
-		functions:         map[string]*sem.Function{},
+		builtins:          map[string]*sem.Intrinsic{},
+		operators:         map[string]*sem.Intrinsic{},
 		enumEntryMatchers: map[*sem.EnumEntry]*sem.EnumMatcher{},
 	}
 	// Declare and resolve all the enumerators
@@ -59,9 +61,15 @@
 			return nil, err
 		}
 	}
-	// Declare and resolve the functions
-	for _, f := range a.Functions {
-		if err := r.function(f); err != nil {
+	// Declare and resolve the builtins
+	for _, f := range a.Builtins {
+		if err := r.intrinsic(f, r.builtins, &r.s.Builtins); err != nil {
+			return nil, err
+		}
+	}
+	// Declare and resolve the operators
+	for _, o := range a.Operators {
+		if err := r.intrinsic(o, r.operators, &r.s.Operators); err != nil {
 			return nil, err
 		}
 	}
@@ -220,18 +228,21 @@
 	return fmt.Errorf("'%v' cannot be used for matcher", a.Name)
 }
 
-// function() resolves a function overload declaration.
-// The the first overload for the function creates and appends the sem.Function
-// to Sem.Functions. Subsequent overloads append their resolved overload to the
-// sem.Function.Overloads list.
-func (r *resolver) function(a ast.FunctionDecl) error {
-	// If this is the first overload of the function, create and register the
-	// semantic function.
-	f := r.functions[a.Name]
-	if f == nil {
-		f = &sem.Function{Name: a.Name}
-		r.functions[a.Name] = f
-		r.s.Functions = append(r.s.Functions, f)
+// intrinsic() resolves a intrinsic overload declaration.
+// The the first overload for the intrinsic creates and appends the sem.Intrinsic
+// to Sem.Intrinsics. Subsequent overloads append their resolved overload to the
+// sem.intrinsic.Overloads list.
+func (r *resolver) intrinsic(
+	a ast.IntrinsicDecl,
+	intrinsicsByName map[string]*sem.Intrinsic,
+	semIntrinsics *[]*sem.Intrinsic) error {
+	// If this is the first overload of the intrinsic, create and register the
+	// semantic intrinsic.
+	intrinsic := intrinsicsByName[a.Name]
+	if intrinsic == nil {
+		intrinsic = &sem.Intrinsic{Name: a.Name}
+		intrinsicsByName[a.Name] = intrinsic
+		*semIntrinsics = append(*semIntrinsics, intrinsic)
 	}
 
 	// Create a new scope for resolving template parameters
@@ -246,7 +257,7 @@
 	// Construct the semantic overload
 	overload := &sem.Overload{
 		Decl:           a,
-		Function:       f,
+		Intrinsic:      intrinsic,
 		Parameters:     make([]sem.Parameter, len(a.Parameters)),
 		TemplateParams: templateParams,
 	}
@@ -285,8 +296,8 @@
 		return fmt.Errorf("%v unknown decoration", a.Decorations[0].Source)
 	}
 
-	// Append the overload to the function
-	f.Overloads = append(f.Overloads, overload)
+	// Append the overload to the intrinsic
+	intrinsic.Overloads = append(intrinsic.Overloads, overload)
 
 	// Sort the template parameters by resolved type. Append these to
 	// sem.Overload.OpenTypes or sem.Overload.OpenNumbers based on their kind.
@@ -307,6 +318,10 @@
 		r.s.MaxOpenNumbers = len(overload.OpenNumbers)
 	}
 
+	if a.Kind == ast.Operator && (len(a.Parameters) < 1 || len(a.Parameters) > 2) {
+		return fmt.Errorf("%v operators must have either 1 or 2 parameters", a.Source)
+	}
+
 	// Resolve the parameters
 	for i, p := range a.Parameters {
 		usage, err := r.fullyQualifiedName(&s, p.Type)
@@ -495,11 +510,11 @@
 }
 
 // calculateUniqueParameterNames() iterates over all the parameters of all
-// overloads, calculating the list of unique parameter names
+// builtin overloads, calculating the list of unique parameter names
 func (r *resolver) calculateUniqueParameterNames() []string {
 	set := map[string]struct{}{"": {}}
 	names := []string{}
-	for _, f := range r.s.Functions {
+	for _, f := range r.s.Builtins {
 		for _, o := range f.Overloads {
 			for _, p := range o.Parameters {
 				if _, dup := set[p.Name]; !dup {
diff --git a/tools/src/cmd/intrinsic-gen/resolver/resolver_test.go b/tools/src/cmd/intrinsic-gen/resolver/resolver_test.go
index 1768dbf..9f3aac0 100644
--- a/tools/src/cmd/intrinsic-gen/resolver/resolver_test.go
+++ b/tools/src/cmd/intrinsic-gen/resolver/resolver_test.go
@@ -139,7 +139,7 @@
 			`
 type f32
 type T<x>
-fn f(T<T<f32>>)`,
+fn f(T< T<f32> >)`,
 			success,
 		}, {
 			`enum E {A A}`,
@@ -299,6 +299,70 @@
 			`file.txt:4:14 cannot use template enum 'E' as template number`,
 		}, {
 			`
+type i
+enum e { a }
+op << (i) -> e`,
+			`file.txt:3:14 cannot use 'e' as return type. Must be a type or template type`,
+		}, {
+			`
+type T<x>
+op << (T<u>)`,
+			`file.txt:2:10 cannot resolve 'u'`,
+		}, {
+			`
+op << ()`,
+			`file.txt:1:4 operators must have either 1 or 2 parameters`,
+		}, {
+			`
+type i
+op << (i, i, i)`,
+			`file.txt:2:4 operators must have either 1 or 2 parameters`,
+		}, {
+			`
+type x
+op << <T>(T<x>)`,
+			`file.txt:2:11 'T' template parameters do not accept template arguments`,
+		}, {
+			`
+type A<N: num>
+type B
+op << (A<B>)`,
+			`file.txt:3:10 cannot use type 'B' as template number`,
+		}, {
+			`
+type A<N>
+enum E { b }
+op << (A<b>)`,
+			`file.txt:3:10 cannot use enum entry 'E.b' as template type`,
+		}, {
+			`
+type T
+type P<N: num>
+match m: T
+op << (P<m>)`,
+			`file.txt:4:10 cannot use type matcher 'm' as template number`,
+		}, {
+			`
+type P<N: num>
+enum E { b }
+op << (P<E>)`,
+			`file.txt:3:10 cannot use enum 'E' as template number`,
+		}, {
+			`
+type P<N: num>
+enum E { a b }
+match m: a | b
+op << (P<m>)`,
+			`file.txt:4:10 cannot use enum matcher 'm' as template number`,
+		}, {
+			`
+type P<N: num>
+enum E { a b }
+match m: a | b
+op << <M: m>(P<M>)`,
+			`file.txt:4:16 cannot use template enum 'E' as template number`,
+		}, {
+			`
 enum E { a }
 type T<X: a>`,
 			`file.txt:2:8 invalid template parameter type 'a'`,
diff --git a/tools/src/cmd/intrinsic-gen/sem/sem.go b/tools/src/cmd/intrinsic-gen/sem/sem.go
index 3d50bc2..9a0f973 100644
--- a/tools/src/cmd/intrinsic-gen/sem/sem.go
+++ b/tools/src/cmd/intrinsic-gen/sem/sem.go
@@ -26,7 +26,8 @@
 	Types        []*Type
 	TypeMatchers []*TypeMatcher
 	EnumMatchers []*EnumMatcher
-	Functions    []*Function
+	Builtins     []*Intrinsic
+	Operators    []*Intrinsic
 	// Maximum number of open-types used across all builtins
 	MaxOpenTypes int
 	// Maximum number of open-numbers used across all builtins
@@ -42,7 +43,8 @@
 		Types:        []*Type{},
 		TypeMatchers: []*TypeMatcher{},
 		EnumMatchers: []*EnumMatcher{},
-		Functions:    []*Function{},
+		Builtins:     []*Intrinsic{},
+		Operators:    []*Intrinsic{},
 	}
 }
 
@@ -121,16 +123,16 @@
 	Name string
 }
 
-// Function describes the overloads of a builtin function
-type Function struct {
+// Intrinsic describes the overloads of a builtin or operator
+type Intrinsic struct {
 	Name      string
 	Overloads []*Overload
 }
 
-// Overload describes a single overload of a function
+// Overload describes a single overload of a builtin or operator
 type Overload struct {
-	Decl             ast.FunctionDecl
-	Function         *Function
+	Decl             ast.IntrinsicDecl
+	Intrinsic        *Intrinsic
 	TemplateParams   []TemplateParam
 	OpenTypes        []*TemplateTypeParam
 	OpenNumbers      []TemplateParam
@@ -164,7 +166,13 @@
 
 // Format implements the fmt.Formatter interface
 func (o Overload) Format(w fmt.State, verb rune) {
-	fmt.Fprintf(w, "fn %v", o.Function.Name)
+	switch o.Decl.Kind {
+	case ast.Builtin:
+		fmt.Fprintf(w, "fn ")
+	case ast.Operator:
+		fmt.Fprintf(w, "op ")
+	}
+	fmt.Fprintf(w, "%v", o.Intrinsic.Name)
 	if len(o.TemplateParams) > 0 {
 		fmt.Fprintf(w, "<")
 		for i, t := range o.TemplateParams {
diff --git a/tools/src/cmd/intrinsic-gen/tok/tok.go b/tools/src/cmd/intrinsic-gen/tok/tok.go
index c15a235..d9e25ad 100644
--- a/tools/src/cmd/intrinsic-gen/tok/tok.go
+++ b/tools/src/cmd/intrinsic-gen/tok/tok.go
@@ -29,12 +29,17 @@
 	String       Kind = "string"
 	Match        Kind = "match"
 	Function     Kind = "fn"
+	Operator     Kind = "op"
 	Type         Kind = "type"
 	Enum         Kind = "enum"
 	Colon        Kind = ":"
 	Comma        Kind = ","
+	Shl          Kind = "<<"
+	Shr          Kind = ">>"
 	Lt           Kind = "<"
+	Le           Kind = "<="
 	Gt           Kind = ">"
+	Ge           Kind = ">="
 	Lbrace       Kind = "{"
 	Rbrace       Kind = "}"
 	Ldeco        Kind = "[["
@@ -43,6 +48,18 @@
 	Rparen       Kind = ")"
 	Or           Kind = "|"
 	Arrow        Kind = "->"
+	Star         Kind = "*"
+	Divide       Kind = "/"
+	Modulo       Kind = "%"
+	Xor          Kind = "^"
+	Plus         Kind = "+"
+	Minus        Kind = "-"
+	And          Kind = "&"
+	AndAnd       Kind = "&&"
+	OrOr         Kind = "||"
+	NotEqual     Kind = "!="
+	Equal        Kind = "=="
+	Assign       Kind = "="
 )
 
 // Invalid represents an invalid token