[tools]: Add intrinsic-gen tool

Parses the intrinsics.def file, then scans the source tree for .tmpl files.
Using both of these, it produces source files for use in tint.

Bug: tint:832
Change-Id: I718114f4c540b9d620899e89d5758f048377ee04
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/52642
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: David Neto <dneto@google.com>
diff --git a/tools/intrinsic-gen b/tools/intrinsic-gen
new file mode 100755
index 0000000..cfbbd4e
--- /dev/null
+++ b/tools/intrinsic-gen
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+# Copyright 2021 The Tint Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -e # Fail on any error.
+
+if [ ! -x "$(which go)" ] ; then
+    echo "error: go needs to be on \$PATH to use $0"
+    exit 1
+fi
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )"
+ROOT_DIR="$( cd "${SCRIPT_DIR}/.." >/dev/null 2>&1 && pwd )"
+BINARY="${SCRIPT_DIR}/bin/intrinsic-gen"
+
+# Rebuild the binary.
+# Note, go caches build artifacts, so this is quick for repeat calls
+pushd "${SCRIPT_DIR}/src/cmd/intrinsic-gen" > /dev/null
+    go build -o "${BINARY}" main.go
+popd > /dev/null
+
+"${BINARY}" "$@"
diff --git a/tools/src/cmd/intrinsic-gen/gen/generate.go b/tools/src/cmd/intrinsic-gen/gen/generate.go
new file mode 100644
index 0000000..a797840
--- /dev/null
+++ b/tools/src/cmd/intrinsic-gen/gen/generate.go
@@ -0,0 +1,179 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gen
+
+import (
+	"fmt"
+	"io"
+	"reflect"
+	"strings"
+	"text/template"
+	"unicode"
+
+	"dawn.googlesource.com/tint/tools/src/cmd/intrinsic-gen/sem"
+)
+
+type generator struct {
+	s *sem.Sem
+}
+
+// Generate executes the template tmpl using the provided semantic
+// information, writing the output to w.
+// See https://golang.org/pkg/text/template/ for documentation on the template
+// syntax.
+func Generate(s *sem.Sem, tmpl string, w io.Writer) error {
+	g := generator{s: s}
+	return g.generate(tmpl, w)
+}
+
+func (g *generator) generate(tmpl string, w io.Writer) error {
+	t, err := template.
+		New("<template>").
+		Funcs(map[string]interface{}{
+			"Map":                   newMap,
+			"Iterate":               iterate,
+			"Title":                 strings.Title,
+			"PascalCase":            pascalCase,
+			"SplitDisplayName":      splitDisplayName,
+			"IsTemplateTypeParam":   is(&sem.TemplateTypeParam{}),
+			"IsTemplateNumberParam": is(&sem.TemplateNumberParam{}),
+			"IsTemplateEnumParam":   is(&sem.TemplateEnumParam{}),
+			"IsFirstIn":             isFirstIn,
+			"IsLastIn":              isLastIn,
+		}).
+		Option("missingkey=error").
+		Parse(tmpl)
+	if err != nil {
+		return err
+	}
+	return t.Execute(w, map[string]interface{}{
+		"Sem": g.s,
+	})
+}
+
+// Map is a simple generic key-value map, which can be used in the template
+type Map map[interface{}]interface{}
+
+func newMap() Map { return Map{} }
+
+// Put adds the key-value pair into the map.
+// Put always returns an empty string so nothing is printed in the template.
+func (m Map) Put(key, value interface{}) string {
+	m[key] = value
+	return ""
+}
+
+// Get looks up and returns the value with the given key. If the map does not
+// contain the given key, then nil is returned.
+func (m Map) Get(key interface{}) interface{} {
+	return m[key]
+}
+
+// is returns a function that returns true if the value passed to the function
+// matches the type of 'ty'.
+func is(ty interface{}) func(interface{}) bool {
+	rty := reflect.TypeOf(ty)
+	return func(v interface{}) bool {
+		return reflect.TypeOf(v) == rty
+	}
+}
+
+// isFirstIn returns true if v is the first element of the given slice.
+func isFirstIn(v, slice interface{}) bool {
+	s := reflect.ValueOf(slice)
+	count := s.Len()
+	if count == 0 {
+		return false
+	}
+	return s.Index(0).Interface() == v
+}
+
+// isFirstIn returns true if v is the last element of the given slice.
+func isLastIn(v, slice interface{}) bool {
+	s := reflect.ValueOf(slice)
+	count := s.Len()
+	if count == 0 {
+		return false
+	}
+	return s.Index(count-1).Interface() == v
+}
+
+// iterate returns a slice of length 'n', with each element equal to its index.
+// Useful for: {{- range Iterate $n -}}<this will be looped $n times>{{end}}
+func iterate(n int) []int {
+	out := make([]int, n)
+	for i := range out {
+		out[i] = i
+	}
+	return out
+}
+
+// pascalCase returns the snake-case string s transformed into 'PascalCase',
+// Rules:
+// * The first letter of the string is capitalized
+// * Characters following an underscore or number are capitalized
+// * Underscores are removed from the returned string
+// See: https://en.wikipedia.org/wiki/Camel_case
+func pascalCase(s string) string {
+	b := strings.Builder{}
+	upper := true
+	for _, r := range s {
+		if r == '_' {
+			upper = true
+			continue
+		}
+		if upper {
+			b.WriteRune(unicode.ToUpper(r))
+			upper = false
+		} else {
+			b.WriteRune(r)
+		}
+		if unicode.IsNumber(r) {
+			upper = true
+		}
+	}
+	return b.String()
+}
+
+// splitDisplayName splits displayName into parts, where text wrapped in {}
+// braces are not quoted and the rest is quoted. This is used to help process
+// the string value of the [[display()]] decoration. For example:
+//   splitDisplayName("vec{N}<{T}>")
+// would return the strings:
+//   [`"vec"`, `N`, `"<"`, `T`, `">"`]
+func splitDisplayName(displayName string) []string {
+	parts := []string{}
+	pending := strings.Builder{}
+	for _, r := range displayName {
+		switch r {
+		case '{':
+			if pending.Len() > 0 {
+				parts = append(parts, fmt.Sprintf(`"%v"`, pending.String()))
+				pending.Reset()
+			}
+		case '}':
+			if pending.Len() > 0 {
+				parts = append(parts, pending.String())
+				pending.Reset()
+			}
+		default:
+			pending.WriteRune(r)
+		}
+	}
+	if pending.Len() > 0 {
+		parts = append(parts, fmt.Sprintf(`"%v"`, pending.String()))
+	}
+	return parts
+}
diff --git a/tools/src/cmd/intrinsic-gen/main.go b/tools/src/cmd/intrinsic-gen/main.go
new file mode 100644
index 0000000..51dc799
--- /dev/null
+++ b/tools/src/cmd/intrinsic-gen/main.go
@@ -0,0 +1,146 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// intrinsic-gen parses the <tint>/src/intrinsics.def file, then scans the
+// project directory for '<file>.tmpl' files, to produce '<file>' source code
+// files.
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"dawn.googlesource.com/tint/tools/src/cmd/intrinsic-gen/gen"
+	"dawn.googlesource.com/tint/tools/src/cmd/intrinsic-gen/parser"
+	"dawn.googlesource.com/tint/tools/src/cmd/intrinsic-gen/resolver"
+	"dawn.googlesource.com/tint/tools/src/fileutils"
+	"dawn.googlesource.com/tint/tools/src/glob"
+)
+
+func main() {
+	if err := run(); err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+}
+
+func showUsage() {
+	fmt.Println(`
+intrinsic-gen generates the intrinsic table for the Tint compiler
+
+intrinsic-gen parses the <tint>/src/intrinsics.def file, then scans the project
+directory for '<file>.tmpl' files, to produce '<file>' source code files.
+
+usage:
+  intrinsic-gen
+
+optional flags:`)
+	flag.PrintDefaults()
+	fmt.Println(``)
+	os.Exit(1)
+}
+
+func run() error {
+	// Load the intrinsics definition file
+	projectRoot := fileutils.ProjectRoot()
+	defPath := filepath.Join(projectRoot, "src/intrinsics.def")
+
+	defSource, err := ioutil.ReadFile(defPath)
+	if err != nil {
+		return err
+	}
+
+	// Parse the definition file to produce an AST
+	ast, err := parser.Parse(string(defSource), defPath)
+	if err != nil {
+		return err
+	}
+
+	// Resolve the AST to produce the semantic info
+	sem, err := resolver.Resolve(ast)
+	if err != nil {
+		return err
+	}
+
+	// Recursively find all the template files in the <tint>/src directory
+	srcDir := filepath.Join(projectRoot, "src")
+	files, err := glob.Scan(srcDir, glob.MustParseConfig(`{
+		"paths": [{"include": [ "**.tmpl" ]}]
+	}`))
+	if err != nil {
+		return err
+	}
+
+	// For each template file...
+	for _, tmplPath := range files {
+		// Make tmplPath absolute
+		tmplPath := filepath.Join(srcDir, tmplPath)
+
+		// Read the template file
+		tmpl, err := ioutil.ReadFile(tmplPath)
+		if err != nil {
+			return fmt.Errorf("failed to open '%v': %w", tmplPath, err)
+		}
+
+		// Write the common file header
+		sb := strings.Builder{}
+		sb.WriteString(header)
+
+		// Write the content generated using the template and semantic info
+		if err := gen.Generate(sem, string(tmpl), &sb); err != nil {
+			return fmt.Errorf("while processing '%v': %w", tmplPath, err)
+		}
+
+		// Create or update the output file if the content has not changed
+		filePath := strings.TrimSuffix(tmplPath, ".tmpl")
+		if err := writeFileIfChanged(filePath, sb.String()); err != nil {
+			return fmt.Errorf("failed to write '%v': %w", filePath, err)
+		}
+	}
+
+	return nil
+}
+
+func writeFileIfChanged(path, content string) error {
+	existing, err := ioutil.ReadFile(path)
+	if err == nil && string(existing) == content {
+		return nil // Not changed
+	}
+	return ioutil.WriteFile(path, []byte(content), 0666)
+}
+
+const header = `// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+////////////////////////////////////////////////////////////////////////////////
+// File generated by tools/intrinsic-gen
+// Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
+`