| // 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. |
| |
| // gen parses the <tint>/src/tint/intrinsics.def file, then scans the |
| // project directory for '<file>.tmpl' files, to produce source code files. |
| package main |
| |
| import ( |
| "flag" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "math/rand" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "reflect" |
| "regexp" |
| "runtime" |
| "strconv" |
| "strings" |
| "text/template" |
| "time" |
| "unicode" |
| |
| "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/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" |
| ) |
| |
| const defProjectRelPath = "src/tint/intrinsics.def" |
| |
| func main() { |
| if err := run(); err != nil { |
| fmt.Println(err) |
| os.Exit(1) |
| } |
| } |
| |
| func showUsage() { |
| fmt.Println(` |
| gen generates the templated code for the Tint compiler |
| |
| gen accepts a list of file paths to the templates to generate. If no templates |
| are explicitly specified, then gen scans the '<dawn>/src/tint' and |
| '<dawn>/test/tint' directories for '<file>.tmpl' files. |
| |
| If the templates use the 'IntrinsicTable' function then gen will parse and |
| resolve the <tint>/src/tint/intrinsics.def file. |
| |
| usage: |
| gen [flags] [template files] |
| |
| optional flags:`) |
| flag.PrintDefaults() |
| fmt.Println(``) |
| os.Exit(1) |
| } |
| |
| func run() error { |
| outputDir := "" |
| verbose := false |
| flag.StringVar(&outputDir, "o", "", "custom output directory (optional)") |
| flag.BoolVar(&verbose, "verbose", false, "print verbose output") |
| flag.Parse() |
| |
| 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 verbose { |
| fmt.Println("processing", relTmplPath) |
| } |
| // Make tmplPath absolute |
| tmplPath := filepath.Join(projectRoot, relTmplPath) |
| tmplDir := filepath.Dir(tmplPath) |
| |
| // Read the template file |
| tmpl, err := ioutil.ReadFile(tmplPath) |
| if err != nil { |
| return fmt.Errorf("failed to open '%v': %w", tmplPath, err) |
| } |
| |
| // 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 { |
| var outPath string |
| if outputDir != "" { |
| relTmplDir := filepath.Dir(relTmplPath) |
| outPath = filepath.Join(outputDir, relTmplDir, relPath) |
| } else { |
| outPath = filepath.Join(tmplDir, relPath) |
| } |
| |
| copyrightYear := time.Now().Year() |
| |
| // Load the old file |
| existing, err := ioutil.ReadFile(outPath) |
| if err == nil { |
| // Look for the existing copyright year |
| if match := copyrightRegex.FindStringSubmatch(string(existing)); len(match) == 2 { |
| if year, err := strconv.Atoi(match[1]); err == nil { |
| copyrightYear = year |
| } |
| } |
| } |
| |
| // Write the common file header |
| if verbose { |
| fmt.Println(" writing", outPath) |
| } |
| sb := strings.Builder{} |
| sb.WriteString(fmt.Sprintf(header, copyrightYear, filepath.ToSlash(relTmplPath))) |
| sb.WriteString(body) |
| content := sb.String() |
| return writeFileIfChanged(outPath, content, string(existing)) |
| } |
| |
| // Write the content generated using the template and semantic info |
| sb := strings.Builder{} |
| if err := generate(string(tmpl), 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": |
| body, err = clangFormat(body, clangFormatPath) |
| if err != nil { |
| return err |
| } |
| } |
| |
| if err := writeFile(outFileName, body); err != nil { |
| return err |
| } |
| } |
| } |
| |
| return nil |
| } |
| |
| // Cache for objects that are expensive to build, and can be reused between templates. |
| type genCache struct { |
| cached struct { |
| sem *sem.Sem // lazily built by sem() |
| intrinsicTable *gen.IntrinsicTable // lazily built by intrinsicTable() |
| permuter *gen.Permuter // lazily built by permute() |
| } |
| } |
| |
| // sem lazily parses and resolves the intrinsic.def file, returning the semantic info. |
| func (g *genCache) sem() (*sem.Sem, error) { |
| if g.cached.sem == nil { |
| // Load the builtins definition file |
| defPath := filepath.Join(fileutils.DawnRoot(), defProjectRelPath) |
| |
| defSource, err := ioutil.ReadFile(defPath) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Parse the definition file to produce an AST |
| ast, err := parser.Parse(string(defSource), defProjectRelPath) |
| 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 |
| } |
| |
| g.cached.sem = sem |
| } |
| return g.cached.sem, nil |
| } |
| |
| // intrinsicTable lazily calls and returns the result of buildIntrinsicTable(), |
| // caching the result for repeated calls. |
| func (g *genCache) intrinsicTable() (*gen.IntrinsicTable, error) { |
| if g.cached.intrinsicTable == nil { |
| sem, err := g.sem() |
| if err != nil { |
| return nil, err |
| } |
| g.cached.intrinsicTable, err = gen.BuildIntrinsicTable(sem) |
| if err != nil { |
| return nil, err |
| } |
| } |
| return g.cached.intrinsicTable, nil |
| } |
| |
| // permute lazily calls buildPermuter(), caching the result for repeated |
| // calls, then passes the argument to Permutator.Permute() |
| func (g *genCache) permute(overload *sem.Overload) ([]gen.Permutation, error) { |
| if g.cached.permuter == nil { |
| sem, err := g.sem() |
| if err != nil { |
| return nil, err |
| } |
| g.cached.permuter, err = gen.NewPermuter(sem) |
| if err != nil { |
| return nil, err |
| } |
| } |
| return g.cached.permuter.Permute(overload) |
| } |
| |
| // writes content to path if the file has changed |
| func writeFileIfChanged(path, newContent, oldContent string) error { |
| if oldContent == newContent { |
| return nil // Not changed |
| } |
| if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil { |
| return fmt.Errorf("failed to create directory for '%v': %w", path, err) |
| } |
| if err := ioutil.WriteFile(path, []byte(newContent), 0666); err != nil { |
| return fmt.Errorf("failed to write file '%v': %w", path, err) |
| } |
| return nil |
| } |
| |
| var copyrightRegex = regexp.MustCompile(`// Copyright (\d+) The`) |
| |
| const header = `// Copyright %v 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/src/cmd/gen |
| // using the template: |
| // %v |
| // |
| // Do not modify this file directly |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| ` |
| |
| type generator struct { |
| template *template.Template |
| 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(tmpl string, cache *genCache, w io.Writer, writeFile WriteFile) error { |
| g := generator{ |
| template: template.New("<template>"), |
| cache: cache, |
| writeFile: writeFile, |
| rnd: rand.New(rand.NewSource(4561123)), |
| } |
| if err := g.bindAndParse(g.template, tmpl); err != nil { |
| return err |
| } |
| return g.template.Execute(w, nil) |
| } |
| |
| func (g *generator) bindAndParse(t *template.Template, text string) error { |
| _, err := t.Funcs(map[string]interface{}{ |
| "Map": newMap, |
| "Iterate": iterate, |
| "Title": strings.Title, |
| "PascalCase": pascalCase, |
| "SplitDisplayName": gen.SplitDisplayName, |
| "Contains": strings.Contains, |
| "HasPrefix": strings.HasPrefix, |
| "HasSuffix": strings.HasSuffix, |
| "TrimPrefix": strings.TrimPrefix, |
| "TrimSuffix": strings.TrimSuffix, |
| "TrimLeft": strings.TrimLeft, |
| "TrimRight": strings.TrimRight, |
| "Split": strings.Split, |
| "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, |
| "OverloadUsesF16": gen.OverloadUsesF16, |
| "IsFirstIn": isFirstIn, |
| "IsLastIn": isLastIn, |
| "Sem": g.cache.sem, |
| "IntrinsicTable": g.cache.intrinsicTable, |
| "Permute": g.cache.permute, |
| "Eval": g.eval, |
| "Import": g.importTmpl, |
| "WriteFile": func(relPath, content string) (string, error) { return "", g.writeFile(relPath, content) }, |
| }).Option("missingkey=error").Parse(text) |
| return err |
| } |
| |
| // eval executes the sub-template with the given name and argument, returning |
| // the generated output |
| func (g *generator) eval(template string, args ...interface{}) (string, error) { |
| target := g.template.Lookup(template) |
| if target == nil { |
| return "", fmt.Errorf("template '%v' not found", template) |
| } |
| sb := strings.Builder{} |
| |
| var err error |
| if len(args) == 1 { |
| err = target.Execute(&sb, args[0]) |
| } else { |
| m := newMap() |
| if len(args)%2 != 0 { |
| return "", fmt.Errorf("Eval expects a single argument or list name-value pairs") |
| } |
| for i := 0; i < len(args); i += 2 { |
| name, ok := args[i].(string) |
| if !ok { |
| return "", fmt.Errorf("Eval argument %v is not a string", i) |
| } |
| m.Put(name, args[i+1]) |
| } |
| err = target.Execute(&sb, m) |
| } |
| |
| if err != nil { |
| return "", fmt.Errorf("while evaluating '%v': %v", template, err) |
| } |
| return sb.String(), nil |
| } |
| |
| // importTmpl parses the template at the given project-relative path, merging |
| // the template definitions into the global namespace. |
| // Note: The body of the template is not executed. |
| func (g *generator) importTmpl(path string) (string, error) { |
| if strings.Contains(path, "..") { |
| return "", fmt.Errorf("import path must not contain '..'") |
| } |
| path = filepath.Join(fileutils.DawnRoot(), path) |
| data, err := ioutil.ReadFile(path) |
| if err != nil { |
| return "", fmt.Errorf("failed to open '%v': %w", path, err) |
| } |
| t := g.template.New("") |
| if err := g.bindAndParse(t, string(data)); err != nil { |
| return "", fmt.Errorf("failed to parse '%v': %w", path, err) |
| } |
| return "", nil |
| } |
| |
| // 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 |
| } |
| |
| // 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 { |
| 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 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() |
| } |
| |
| // 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 "" |
| } |