|  | // Copyright 2021 The Dawn & Tint Authors | 
|  | // | 
|  | // Redistribution and use in source and binary forms, with or without | 
|  | // modification, are permitted provided that the following conditions are met: | 
|  | // | 
|  | // 1. Redistributions of source code must retain the above copyright notice, this | 
|  | //    list of conditions and the following disclaimer. | 
|  | // | 
|  | // 2. Redistributions in binary form must reproduce the above copyright notice, | 
|  | //    this list of conditions and the following disclaimer in the documentation | 
|  | //    and/or other materials provided with the distribution. | 
|  | // | 
|  | // 3. Neither the name of the copyright holder nor the names of its | 
|  | //    contributors may be used to endorse or promote products derived from | 
|  | //    this software without specific prior written permission. | 
|  | // | 
|  | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | 
|  | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | 
|  | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | 
|  | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | 
|  | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | 
|  | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | 
|  | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | 
|  | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | 
|  | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 
|  | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 
|  |  | 
|  | package templates | 
|  |  | 
|  | import ( | 
|  | "context" | 
|  | "flag" | 
|  | "fmt" | 
|  | "io" | 
|  | "math/rand" | 
|  | "os" | 
|  | "os/exec" | 
|  | "path/filepath" | 
|  | "reflect" | 
|  | "runtime" | 
|  | "strings" | 
|  |  | 
|  | "dawn.googlesource.com/dawn/tools/src/cmd/gen/common" | 
|  | "dawn.googlesource.com/dawn/tools/src/container" | 
|  | "dawn.googlesource.com/dawn/tools/src/fileutils" | 
|  | "dawn.googlesource.com/dawn/tools/src/glob" | 
|  | "dawn.googlesource.com/dawn/tools/src/template" | 
|  | "dawn.googlesource.com/dawn/tools/src/tint/intrinsic/gen" | 
|  | "dawn.googlesource.com/dawn/tools/src/tint/intrinsic/parser" | 
|  | "dawn.googlesource.com/dawn/tools/src/tint/intrinsic/resolver" | 
|  | "dawn.googlesource.com/dawn/tools/src/tint/intrinsic/sem" | 
|  | ) | 
|  |  | 
|  | func init() { | 
|  | common.Register(&Cmd{}) | 
|  | } | 
|  |  | 
|  | type Cmd struct { | 
|  | } | 
|  |  | 
|  | func (Cmd) Name() string { | 
|  | return "templates" | 
|  | } | 
|  |  | 
|  | func (Cmd) Desc() string { | 
|  | return `templates generates files from <file>.tmpl files found in the Tint source and test directories` | 
|  | } | 
|  |  | 
|  | func (c *Cmd) RegisterFlags(ctx context.Context, cfg *common.Config) ([]string, error) { | 
|  | return nil, nil | 
|  | } | 
|  |  | 
|  | func (c Cmd) Run(ctx context.Context, cfg *common.Config) error { | 
|  | staleFiles := common.StaleFiles{} | 
|  | projectRoot := fileutils.DawnRoot() | 
|  |  | 
|  | // Find clang-format | 
|  | clangFormatPath := findClangFormat(projectRoot) | 
|  | if clangFormatPath == "" { | 
|  | return fmt.Errorf("cannot find clang-format in <dawn>/buildtools nor PATH") | 
|  | } | 
|  |  | 
|  | files := flag.Args() | 
|  | if len(files) == 0 { | 
|  | // Recursively find all the template files in the <dawn>/src/tint and | 
|  | // <dawn>/test/tint and directories | 
|  | var err error | 
|  | files, err = glob.Scan(projectRoot, glob.MustParseConfig(`{ | 
|  | "paths": [{"include": [ | 
|  | "src/tint/**.tmpl", | 
|  | "test/tint/**.tmpl" | 
|  | ]}] | 
|  | }`)) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | } else { | 
|  | // Make all template file paths project-relative | 
|  | for i, f := range files { | 
|  | abs, err := filepath.Abs(f) | 
|  | if err != nil { | 
|  | return fmt.Errorf("failed to get absolute file path for '%v': %w", f, err) | 
|  | } | 
|  | if !strings.HasPrefix(abs, projectRoot) { | 
|  | return fmt.Errorf("template '%v' is not under project root '%v'", abs, projectRoot) | 
|  | } | 
|  | rel, err := filepath.Rel(projectRoot, abs) | 
|  | if err != nil { | 
|  | return fmt.Errorf("failed to get project relative file path for '%v': %w", f, err) | 
|  | } | 
|  | files[i] = rel | 
|  | } | 
|  | } | 
|  |  | 
|  | cache := &genCache{} | 
|  |  | 
|  | // For each template file... | 
|  | for _, relTmplPath := range files { // relative to project root | 
|  | if cfg.Flags.Verbose { | 
|  | fmt.Println("processing", relTmplPath) | 
|  | } | 
|  | // Make tmplPath absolute | 
|  | tmplPath := filepath.Join(projectRoot, relTmplPath) | 
|  | tmplDir := filepath.Dir(tmplPath) | 
|  |  | 
|  | // Create or update the file at relPath if the file content has changed, | 
|  | // preserving the copyright year in the header. | 
|  | // relPath is a path relative to the template | 
|  | writeFile := func(relPath, body string) error { | 
|  | outPath := filepath.Join(tmplDir, relPath) | 
|  |  | 
|  | // Load the old file | 
|  | existing, err := os.ReadFile(outPath) | 
|  | if err != nil { | 
|  | existing = nil | 
|  | } | 
|  |  | 
|  | // Write the common file header | 
|  | if cfg.Flags.Verbose { | 
|  | fmt.Println("  writing", outPath) | 
|  | } | 
|  | sb := strings.Builder{} | 
|  | sb.WriteString(common.Header(string(existing), filepath.ToSlash(relTmplPath), "//")) | 
|  | sb.WriteString("\n") | 
|  | sb.WriteString(body) | 
|  | oldContent, newContent := string(existing), sb.String() | 
|  |  | 
|  | if oldContent != newContent { | 
|  | if cfg.Flags.CheckStale { | 
|  | staleFiles = append(staleFiles, outPath) | 
|  | } else { | 
|  | if err := os.MkdirAll(filepath.Dir(outPath), 0777); err != nil { | 
|  | return fmt.Errorf("failed to create directory for '%v': %w", outPath, err) | 
|  | } | 
|  | if err := os.WriteFile(outPath, []byte(newContent), 0666); err != nil { | 
|  | return fmt.Errorf("failed to write file '%v': %w", outPath, err) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // Write the content generated using the template and semantic info | 
|  | sb := strings.Builder{} | 
|  | if err := generate(tmplPath, cache, &sb, writeFile); err != nil { | 
|  | return fmt.Errorf("while processing '%v': %w", tmplPath, err) | 
|  | } | 
|  |  | 
|  | if body := sb.String(); body != "" { | 
|  | _, tmplFileName := filepath.Split(tmplPath) | 
|  | outFileName := strings.TrimSuffix(tmplFileName, ".tmpl") | 
|  |  | 
|  | switch filepath.Ext(outFileName) { | 
|  | case ".cc", ".h", ".inl": | 
|  | var err error | 
|  | body, err = clangFormat(body, clangFormatPath) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | } | 
|  |  | 
|  | if err := writeFile(outFileName, body); err != nil { | 
|  | return err | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | if len(staleFiles) > 0 { | 
|  | return staleFiles | 
|  | } | 
|  |  | 
|  | return nil | 
|  | } | 
|  |  | 
|  | type intrinsicCache struct { | 
|  | path           string | 
|  | cachedSem      *sem.Sem            // lazily built by sem() | 
|  | cachedTable    *gen.IntrinsicTable // lazily built by intrinsicTable() | 
|  | cachedPermuter *gen.Permuter       // lazily built by permute() | 
|  | } | 
|  |  | 
|  | // Sem lazily parses and resolves the intrinsic.def file, returning the semantic info. | 
|  | func (i *intrinsicCache) Sem() (*sem.Sem, error) { | 
|  | if i.cachedSem == nil { | 
|  | // Load the intrinsic definition file | 
|  | defPath := filepath.Join(fileutils.DawnRoot(), i.path) | 
|  |  | 
|  | defSource, err := os.ReadFile(defPath) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  |  | 
|  | // Parse the definition file to produce an AST | 
|  | ast, err := parser.Parse(string(defSource), i.path) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  |  | 
|  | // Resolve the AST to produce the semantic info | 
|  | sem, err := resolver.Resolve(ast) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  |  | 
|  | i.cachedSem = sem | 
|  | } | 
|  | return i.cachedSem, nil | 
|  | } | 
|  |  | 
|  | // Table lazily calls and returns the result of BuildIntrinsicTable(), | 
|  | // caching the result for repeated calls. | 
|  | func (i *intrinsicCache) Table() (*gen.IntrinsicTable, error) { | 
|  | if i.cachedTable == nil { | 
|  | sem, err := i.Sem() | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | i.cachedTable, err = gen.BuildIntrinsicTable(sem) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | } | 
|  | return i.cachedTable, nil | 
|  | } | 
|  |  | 
|  | // Permute lazily calls NewPermuter(), caching the result for repeated calls, | 
|  | // then passes the argument to Permutator.Permute() | 
|  | func (i *intrinsicCache) Permute(overload *sem.Overload) ([]gen.Permutation, error) { | 
|  | if i.cachedPermuter == nil { | 
|  | sem, err := i.Sem() | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | i.cachedPermuter, err = gen.NewPermuter(sem) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | } | 
|  | return i.cachedPermuter.Permute(overload) | 
|  | } | 
|  |  | 
|  | // Cache for objects that are expensive to build, and can be reused between templates. | 
|  | type genCache struct { | 
|  | intrinsicsCache container.Map[string, *intrinsicCache] | 
|  | } | 
|  |  | 
|  | func (g *genCache) intrinsics(path string) *intrinsicCache { | 
|  | if g.intrinsicsCache == nil { | 
|  | g.intrinsicsCache = container.NewMap[string, *intrinsicCache]() | 
|  | } | 
|  | i := g.intrinsicsCache[path] | 
|  | if i == nil { | 
|  | i = &intrinsicCache{path: path} | 
|  | g.intrinsicsCache[path] = i | 
|  | } | 
|  | return i | 
|  | } | 
|  |  | 
|  | type generator struct { | 
|  | cache     *genCache | 
|  | writeFile WriteFile | 
|  | rnd       *rand.Rand | 
|  | } | 
|  |  | 
|  | // WriteFile is a function that Generate() may call to emit a new file from a | 
|  | // template. | 
|  | // relPath is the relative path from the currently executing template. | 
|  | // content is the file content to write. | 
|  | type WriteFile func(relPath, content string) error | 
|  |  | 
|  | // generate executes the template tmpl, writing the output to w. | 
|  | // See https://golang.org/pkg/text/template/ for documentation on the template | 
|  | // syntax. | 
|  | func generate(tmplPath string, cache *genCache, w io.Writer, writeFile WriteFile) error { | 
|  | g := generator{ | 
|  | cache:     cache, | 
|  | writeFile: writeFile, | 
|  | rnd:       rand.New(rand.NewSource(4561123)), | 
|  | } | 
|  |  | 
|  | funcs := map[string]any{ | 
|  | "SplitDisplayName":                    gen.SplitDisplayName, | 
|  | "Scramble":                            g.scramble, | 
|  | "IsEnumEntry":                         is(sem.EnumEntry{}), | 
|  | "IsEnumMatcher":                       is(sem.EnumMatcher{}), | 
|  | "IsFQN":                               is(sem.FullyQualifiedName{}), | 
|  | "IsInt":                               is(1), | 
|  | "IsTemplateEnumParam":                 is(sem.TemplateEnumParam{}), | 
|  | "IsTemplateNumberParam":               is(sem.TemplateNumberParam{}), | 
|  | "IsTemplateTypeParam":                 is(sem.TemplateTypeParam{}), | 
|  | "IsType":                              is(sem.Type{}), | 
|  | "ElementType":                         gen.ElementType, | 
|  | "DeepestElementType":                  gen.DeepestElementType, | 
|  | "IsAbstract":                          gen.IsAbstract, | 
|  | "IsDeclarable":                        gen.IsDeclarable, | 
|  | "IsHostShareable":                     gen.IsHostShareable, | 
|  | "OverloadUsesF16":                     gen.OverloadUsesF16, | 
|  | "OverloadUsesReadWriteStorageTexture": gen.OverloadUsesReadWriteStorageTexture, | 
|  | "IsFirstIn":                           isFirstIn, | 
|  | "IsLastIn":                            isLastIn, | 
|  | "LoadIntrinsics":                      func(path string) *intrinsicCache { return g.cache.intrinsics(path) }, | 
|  | "WriteFile":                           func(relPath, content string) (string, error) { return "", g.writeFile(relPath, content) }, | 
|  | } | 
|  | t, err := template.FromFile(tmplPath) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | return t.Run(w, nil, funcs) | 
|  | } | 
|  |  | 
|  | // scramble randomly modifies the input string so that it is no longer equal to | 
|  | // any of the strings in 'avoid'. | 
|  | func (g *generator) scramble(str string, avoid container.Set[string]) (string, error) { | 
|  | bytes := []byte(str) | 
|  | passes := g.rnd.Intn(5) + 1 | 
|  |  | 
|  | const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz" | 
|  |  | 
|  | char := func() byte { return chars[g.rnd.Intn(len(chars))] } | 
|  | replace := func(at int) { bytes[at] = char() } | 
|  | delete := func(at int) { bytes = append(bytes[:at], bytes[at+1:]...) } | 
|  | insert := func(at int) { bytes = append(append(bytes[:at], char()), bytes[at:]...) } | 
|  |  | 
|  | for i := 0; i < passes || avoid.Contains(string(bytes)); i++ { | 
|  | if len(bytes) > 0 { | 
|  | at := g.rnd.Intn(len(bytes)) | 
|  | switch g.rnd.Intn(3) { | 
|  | case 0: | 
|  | replace(at) | 
|  | case 1: | 
|  | delete(at) | 
|  | case 2: | 
|  | insert(at) | 
|  | } | 
|  | } else { | 
|  | insert(0) | 
|  | } | 
|  | } | 
|  | return string(bytes), nil | 
|  | } | 
|  |  | 
|  | // is returns a function that returns true if the value passed to the function | 
|  | // matches the type of 'ty'. | 
|  | func is(ty any) func(any) bool { | 
|  | rty := reflect.TypeOf(ty) | 
|  | return func(v any) bool { | 
|  | ty := reflect.TypeOf(v) | 
|  | return ty == rty || ty == reflect.PtrTo(rty) | 
|  | } | 
|  | } | 
|  |  | 
|  | // isFirstIn returns true if v is the first element of the given slice. | 
|  | func isFirstIn(v, slice any) 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 any) bool { | 
|  | s := reflect.ValueOf(slice) | 
|  | count := s.Len() | 
|  | if count == 0 { | 
|  | return false | 
|  | } | 
|  | return s.Index(count-1).Interface() == v | 
|  | } | 
|  |  | 
|  | // Invokes the clang-format executable at 'exe' to format the file content 'in'. | 
|  | // Returns the formatted file. | 
|  | func clangFormat(in, exe string) (string, error) { | 
|  | cmd := exec.Command(exe) | 
|  | cmd.Stdin = strings.NewReader(in) | 
|  | out, err := cmd.CombinedOutput() | 
|  | if err != nil { | 
|  | return "", fmt.Errorf("clang-format failed:\n%v\n%v", string(out), err) | 
|  | } | 
|  | return string(out), nil | 
|  | } | 
|  |  | 
|  | // Looks for clang-format in the 'buildtools' directory, falling back to PATH | 
|  | func findClangFormat(projectRoot string) string { | 
|  | var path string | 
|  | switch runtime.GOOS { | 
|  | case "linux": | 
|  | path = filepath.Join(projectRoot, "buildtools/linux64/clang-format") | 
|  | case "darwin": | 
|  | path = filepath.Join(projectRoot, "buildtools/mac/clang-format") | 
|  | case "windows": | 
|  | path = filepath.Join(projectRoot, "buildtools/win/clang-format.exe") | 
|  | } | 
|  | if fileutils.IsExe(path) { | 
|  | return path | 
|  | } | 
|  | var err error | 
|  | path, err = exec.LookPath("clang-format") | 
|  | if err == nil { | 
|  | return path | 
|  | } | 
|  | return "" | 
|  | } |