| // 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" | |
| "math/rand" | |
| "path/filepath" | |
| "reflect" | |
| "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/oswrapper" | |
| "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(&CmdSources{}) | |
| common.Register(&CmdTests{}) | |
| } | |
| // CmdSources implements the "sources" command. | |
| type CmdSources struct{} | |
| func (c *CmdSources) Name() string { return "sources" } | |
| func (c *CmdSources) Desc() string { | |
| return `sources generates files from <file>.tmpl files found in the Tint source directory` | |
| } | |
| func (c *CmdSources) RegisterFlags(ctx context.Context, cfg *common.Config) ([]string, error) { | |
| return nil, nil | |
| } | |
| func (c *CmdSources) Run(ctx context.Context, cfg *common.Config) error { | |
| args := flag.Args() | |
| outputDir := "" | |
| if len(args) == 1 { | |
| outputDir = args[0] | |
| } else if len(args) > 1 { | |
| return fmt.Errorf("too many arguments") | |
| } | |
| return runTemplates(ctx, cfg, "src/tint", outputDir) | |
| } | |
| // CmdTests implements the "tests" command. | |
| type CmdTests struct{} | |
| func (c *CmdTests) Name() string { return "tests" } | |
| func (c *CmdTests) Desc() string { | |
| return `tests generates files from <file>.tmpl files found in the Tint test directory` | |
| } | |
| func (c *CmdTests) RegisterFlags(ctx context.Context, cfg *common.Config) ([]string, error) { | |
| return nil, nil | |
| } | |
| func (c *CmdTests) Run(ctx context.Context, cfg *common.Config) error { | |
| if len(flag.Args()) > 0 { | |
| return fmt.Errorf("arguments are not supported") | |
| } | |
| return runTemplates(ctx, cfg, "test/tint", "") | |
| } | |
| // TODO(crbug.com/344014313): Add unittest coverage. | |
| // runTemplates executes the templates matching the defaultIncludes patterns. | |
| func runTemplates(ctx context.Context, cfg *common.Config, dirPrefix string, outputDir string) error { | |
| staleFiles := common.StaleFiles{} | |
| projectRoot := filepath.Join(fileutils.DawnRoot(cfg.OsWrapper), dirPrefix) | |
| configStr := `{ | |
| "paths": [{"include": ["**.tmpl"]}] | |
| }` | |
| files, err := glob.Scan(projectRoot, glob.MustParseConfig(configStr), cfg.OsWrapper) | |
| if err != nil { | |
| return err | |
| } | |
| cache := &genCache{ | |
| fsReader: cfg.OsWrapper, | |
| } | |
| // 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, commentPrefix string) error { | |
| if strings.TrimSpace(body) == "" { | |
| // Don't write empty files | |
| return nil | |
| } | |
| var outPath string | |
| if outputDir != "" { | |
| outPath = filepath.Join(outputDir, dirPrefix, filepath.Dir(relTmplPath), relPath) | |
| } else { | |
| outPath = filepath.Join(tmplDir, relPath) | |
| } | |
| // Load the old file | |
| existing, err := cfg.OsWrapper.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(filepath.Join(dirPrefix, relTmplPath)), commentPrefix)) | |
| 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 := cfg.OsWrapper.MkdirAll(filepath.Dir(outPath), 0777); err != nil { | |
| return fmt.Errorf("failed to create directory for '%v': %w", outPath, err) | |
| } | |
| if err := cfg.OsWrapper.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 | |
| _, tmplFileName := filepath.Split(tmplPath) | |
| outPath := strings.TrimSuffix(tmplFileName, ".tmpl") | |
| if err := generate(tmplPath, outPath, cache, writeFile); err != nil { | |
| return fmt.Errorf("while processing '%v': %w", tmplPath, 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.Permutator // lazily built by permute() | |
| fsReader oswrapper.FilesystemReader // Stored reference to a FilesystemReader. | |
| } | |
| // 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.fsReader), i.path) | |
| defSource, err := i.fsReader.ReadFile(defPath) | |
| if err != nil { | |
| return nil, err | |
| } | |
| // Parse the definition file to produce an AST | |
| ast, err := parser.Parse(string(defSource), i.path, i.fsReader) | |
| 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.NewPermutator(sem) | |
| if err != nil { | |
| return nil, err | |
| } | |
| } | |
| 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. | |
| type genCache struct { | |
| intrinsicsCache container.Map[string, *intrinsicCache] | |
| fsReader oswrapper.FilesystemReader | |
| } | |
| 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, fsReader: g.fsReader} | |
| g.intrinsicsCache[path] = i | |
| } | |
| return i | |
| } | |
| type generator struct { | |
| cache *genCache | |
| writeFile WriteFile | |
| rnd *rand.Rand | |
| commentPrefix string | |
| } | |
| // setCommentPrefix sets the prefix used for comments, as used by the template | |
| func (g *generator) setCommentPrefix(commentPrefix string) string { | |
| g.commentPrefix = commentPrefix | |
| return "" | |
| } | |
| // 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. | |
| // comment is the prefix used for line comments | |
| type WriteFile func(relPath, content, comment string) error | |
| // generate executes the template tmpl, calling writeFile with the output. | |
| // See https://golang.org/pkg/text/template/ for documentation on the template | |
| // syntax. | |
| func generate(tmplPath, outPath string, cache *genCache, writeFile WriteFile) error { | |
| g := generator{ | |
| cache: cache, | |
| writeFile: writeFile, | |
| rnd: rand.New(rand.NewSource(4561123)), | |
| commentPrefix: "//", | |
| } | |
| funcs := map[string]any{ | |
| "SetCommentPrefix": g.setCommentPrefix, | |
| "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, | |
| "OverloadUsesType": gen.OverloadUsesType, | |
| "OverloadUsesReadWriteStorageTexture": gen.OverloadUsesReadWriteStorageTexture, | |
| "OverloadNeedsDesktopGLSL": gen.OverloadNeedsDesktopGLSL, | |
| "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, g.commentPrefix) | |
| }, | |
| } | |
| t, err := template.FromFile(tmplPath, cache.fsReader) | |
| if err != nil { | |
| return err | |
| } | |
| w := &strings.Builder{} | |
| if err := t.Run(w, nil, funcs); err != nil { | |
| return err | |
| } | |
| return writeFile(outPath, w.String(), g.commentPrefix) | |
| } | |
| // 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.PointerTo(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 | |
| } |