cuelang.org/go@v0.13.0/internal/filetypes/generate.go (about)

     1  // Copyright 2025 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  //go:build ignore
    16  
    17  package main
    18  
    19  import (
    20  	"bytes"
    21  	"cmp"
    22  	_ "embed"
    23  	"fmt"
    24  	goformat "go/format"
    25  	"iter"
    26  	"log"
    27  	"maps"
    28  	"os"
    29  	"path/filepath"
    30  	"slices"
    31  	"sort"
    32  	"strings"
    33  	"text/template"
    34  
    35  	"cuelang.org/go/cue"
    36  	"cuelang.org/go/cue/build"
    37  	"cuelang.org/go/cue/cuecontext"
    38  	"cuelang.org/go/cue/errors"
    39  	"cuelang.org/go/cue/format"
    40  	"cuelang.org/go/cue/token"
    41  	"cuelang.org/go/internal/filetypes"
    42  	"cuelang.org/go/internal/filetypes/internal"
    43  	"cuelang.org/go/internal/filetypes/internal/genfunc"
    44  	"cuelang.org/go/internal/filetypes/internal/genstruct"
    45  )
    46  
    47  type tmplParams struct {
    48  	TagTypes                   map[string]filetypes.TagType
    49  	ToFileParams               *genToFileParams
    50  	ToFileResult               *genToFileResult
    51  	FromFileParams             *genFromFileParams
    52  	FromFileResult             *genFromFileResult
    53  	SubsidiaryBoolTagFuncCount int
    54  	SubsidiaryTagFuncCount     int
    55  	Data                       string
    56  	// Generated is used by the generation code to avoid
    57  	// generating the same global identifier twice.
    58  	Generated map[string]bool
    59  }
    60  
    61  var (
    62  	//go:embed types_gen.go.tmpl
    63  	typesGenCode string
    64  
    65  	//go:embed types.cue
    66  	typesCUE string
    67  )
    68  
    69  var tmpl = template.Must(template.New("types_gen.go.tmpl").Parse(typesGenCode))
    70  
    71  type tagInfo struct {
    72  	name  string
    73  	typ   filetypes.TagType
    74  	value cue.Value // only set when kind is filetypes.TagTopLevel
    75  }
    76  
    77  // fileResult represents a possible result for toFile.
    78  type fileResult struct {
    79  	bits uint64
    80  
    81  	mode     string
    82  	fileVal  cue.Value
    83  	filename string
    84  	file     *build.File
    85  
    86  	subsidiaryTags     cue.Value
    87  	subsidiaryBoolTags cue.Value
    88  
    89  	subsidiaryBoolTagFuncIndex int // valid if subsidiaryBoolTags.Exists
    90  	subsidiaryTagFuncIndex     int // valid if subsidiaryTags.Exists
    91  
    92  	err internal.ErrorKind
    93  
    94  	tags []string
    95  }
    96  
    97  func (r *fileResult) appendRecord(data []byte, paramsStruct *genToFileParams, resultStruct *genToFileResult) []byte {
    98  	recordSize := paramsStruct.Size() + resultStruct.Size()
    99  	data = slices.Grow(data, recordSize)
   100  	data = data[:len(data)+recordSize]
   101  	record := data[len(data)-recordSize:]
   102  
   103  	// Write the key part of the record.
   104  	param := slices.Clip(record[:paramsStruct.Size()])
   105  	paramsStruct.FileExt.Put(param, fileExt(r.filename))
   106  	paramsStruct.Tags.Put(param, genstruct.ElemsFromBits(r.bits, r.tags))
   107  	var mode filetypes.Mode
   108  	switch r.mode {
   109  	case "input":
   110  		mode = filetypes.Input
   111  	case "export":
   112  		mode = filetypes.Export
   113  	case "def":
   114  		mode = filetypes.Def
   115  	case "eval":
   116  		mode = filetypes.Eval
   117  	default:
   118  		panic(fmt.Errorf("unknown mode %q", r.mode))
   119  	}
   120  	paramsStruct.Mode.Put(param, mode)
   121  
   122  	result := slices.Clip(record[paramsStruct.Size():])
   123  	// Write the result part of the record.
   124  	if r.err != internal.ErrNoError {
   125  		resultStruct.Error.Put(result, r.err)
   126  		return data
   127  	}
   128  	resultStruct.Encoding.Put(result, r.file.Encoding)
   129  	resultStruct.Interpretation.Put(result, r.file.Interpretation)
   130  	resultStruct.Form.Put(result, r.file.Form)
   131  	if r.subsidiaryBoolTags.Exists() {
   132  		resultStruct.SubsidiaryBoolTagFuncIndex.Put(result, r.subsidiaryBoolTagFuncIndex+1)
   133  	}
   134  	if r.subsidiaryTags.Exists() {
   135  		resultStruct.SubsidiaryTagFuncIndex.Put(result, r.subsidiaryTagFuncIndex+1)
   136  	}
   137  	return data
   138  }
   139  
   140  func main() {
   141  	if err := generate(); err != nil {
   142  		log.Fatal(err)
   143  	}
   144  }
   145  
   146  var top cue.Value
   147  
   148  func generate() error {
   149  	ctx := cuecontext.New()
   150  	top = ctx.CompileString("_")
   151  	rootVal := ctx.CompileString(typesCUE, cue.Filename("types.cue"))
   152  
   153  	toFile, err := generateToFile(rootVal)
   154  	if err != nil {
   155  		return err
   156  	}
   157  	fromFile, err := generateFromFile(rootVal)
   158  	if err != nil {
   159  		return err
   160  	}
   161  	if err := generateCode(toFile, fromFile); err != nil {
   162  		return err
   163  	}
   164  	return nil
   165  }
   166  
   167  // toFileInfo holds the information needed to generate the toFile implementation code.
   168  type toFileInfo struct {
   169  	paramsStruct            *genToFileParams
   170  	resultStruct            *genToFileResult
   171  	tagTypes                map[string]filetypes.TagType
   172  	subsidiaryBoolTagsByCUE map[string]cueValue
   173  	subsidiaryTagsByCUE     map[string]cueValue
   174  	subsidiaryBoolTagKeys   []string
   175  	subsidiaryTagKeys       []string
   176  }
   177  
   178  // fromFileInfo holds the information needed to generate the fromFile implementation code.
   179  type fromFileInfo struct {
   180  	paramsStruct *genFromFileParams
   181  	resultStruct *genFromFileResult
   182  }
   183  
   184  func generateToFile(rootVal cue.Value) (toFileInfo, error) {
   185  	count := 0
   186  	errCount := 0
   187  	tags, topLevelTags, _ := allTags(rootVal)
   188  
   189  	results := slices.Collect(allCombinations(rootVal, topLevelTags, tags))
   190  	subsidiaryTagsByCUE := make(map[string]cueValue)
   191  	subsidiaryTagKeysMap := make(map[string]bool)
   192  	subsidiaryBoolTagsByCUE := make(map[string]cueValue)
   193  	subsidiaryBoolTagKeysMap := make(map[string]bool)
   194  	for i, r := range results {
   195  		if v := r.subsidiaryBoolTags; v.Exists() {
   196  			results[i].subsidiaryBoolTagFuncIndex = addCUELogic(v, subsidiaryBoolTagsByCUE, subsidiaryBoolTagKeysMap)
   197  		}
   198  		if v := r.subsidiaryTags; v.Exists() {
   199  			results[i].subsidiaryTagFuncIndex = addCUELogic(v, subsidiaryTagsByCUE, subsidiaryTagKeysMap)
   200  		}
   201  	}
   202  	subsidiaryBoolTagKeys := slices.Sorted(maps.Keys(subsidiaryBoolTagKeysMap))
   203  	subsidiaryTagKeys := slices.Sorted(maps.Keys(subsidiaryTagKeysMap))
   204  	toFileParams := newToFileParamsStruct(
   205  		topLevelTags,
   206  		// Note: add ".unknown" as a proxy for any unknown file extension,
   207  		// and make sure that the empty file extension is also present
   208  		// even though it's not mentioned in the extensions struct.
   209  		append(allKeys[string](rootVal, "all", "extensions"), ".unknown", ""),
   210  	)
   211  	toFileResult := newToFileResultStruct(
   212  		append(allKeys[build.Encoding](rootVal, "all", "encodings"), ""),
   213  		append(allKeys[build.Interpretation](rootVal, "all", "interpretations"), ""),
   214  		append(allKeys[build.Form](rootVal, "all", "forms"), ""),
   215  		len(subsidiaryBoolTagsByCUE),
   216  		len(subsidiaryTagsByCUE),
   217  	)
   218  
   219  	tagTypes := make(map[string]filetypes.TagType)
   220  	for name, info := range tags {
   221  		tagTypes[name] = info.typ
   222  	}
   223  
   224  	var recordData []byte
   225  	for _, r := range results {
   226  		count++
   227  		if r.err != internal.ErrNoError {
   228  			errCount++
   229  		}
   230  		recordData = r.appendRecord(recordData, toFileParams, toFileResult)
   231  	}
   232  	genstruct.SortRecords(recordData, toFileParams.Size()+toFileResult.Size(), toFileParams.Size())
   233  	if err := os.WriteFile("fileinfo.dat", recordData, 0o666); err != nil {
   234  		return toFileInfo{}, err
   235  	}
   236  
   237  	return toFileInfo{
   238  		paramsStruct:            toFileParams,
   239  		resultStruct:            toFileResult,
   240  		tagTypes:                tagTypes,
   241  		subsidiaryBoolTagsByCUE: subsidiaryBoolTagsByCUE,
   242  		subsidiaryTagsByCUE:     subsidiaryTagsByCUE,
   243  		subsidiaryBoolTagKeys:   subsidiaryBoolTagKeys,
   244  		subsidiaryTagKeys:       subsidiaryTagKeys,
   245  	}, nil
   246  }
   247  
   248  func generateCode(
   249  	toFile toFileInfo,
   250  	fromFile fromFileInfo,
   251  ) error {
   252  	params := tmplParams{
   253  		ToFileParams:               toFile.paramsStruct,
   254  		ToFileResult:               toFile.resultStruct,
   255  		FromFileParams:             fromFile.paramsStruct,
   256  		FromFileResult:             fromFile.resultStruct,
   257  		TagTypes:                   toFile.tagTypes,
   258  		Data:                       "fileInfoDataBytes",
   259  		SubsidiaryBoolTagFuncCount: len(toFile.subsidiaryBoolTagsByCUE),
   260  		SubsidiaryTagFuncCount:     len(toFile.subsidiaryTagsByCUE),
   261  		Generated:                  make(map[string]bool),
   262  	}
   263  	var buf bytes.Buffer
   264  	if err := tmpl.Execute(&buf, params); err != nil {
   265  		return err
   266  	}
   267  
   268  	// Now generate the subsidiary tag logic; we generate
   269  	// a type for each class of subsidiary tag, containing all the possible
   270  	// tags for that class. Then we generate a function for each
   271  	// distinct piece of CUE logic that implements that logic
   272  	// in Go.
   273  	genfunc.GenerateGoTypeForFields(&buf, "subsidiaryTags", toFile.subsidiaryTagKeys, "string")
   274  	genfunc.GenerateGoTypeForFields(&buf, "subsidiaryBoolTags", toFile.subsidiaryBoolTagKeys, "bool")
   275  
   276  	for _, k := range slices.Sorted(maps.Keys(toFile.subsidiaryTagsByCUE)) {
   277  		v := toFile.subsidiaryTagsByCUE[k]
   278  		genfunc.GenerateGoFuncForCUEStruct(&buf, fmt.Sprintf("unifySubsidiaryTags_%d", v.index), "subsidiaryTags", v.v, toFile.subsidiaryTagKeys, "string")
   279  	}
   280  
   281  	for _, k := range slices.Sorted(maps.Keys(toFile.subsidiaryBoolTagsByCUE)) {
   282  		v := toFile.subsidiaryBoolTagsByCUE[k]
   283  		genfunc.GenerateGoFuncForCUEStruct(&buf, fmt.Sprintf("unifySubsidiaryBoolTags_%d", v.index), "subsidiaryBoolTags", v.v, toFile.subsidiaryBoolTagKeys, "bool")
   284  	}
   285  
   286  	data, err := goformat.Source(buf.Bytes())
   287  	if err != nil {
   288  		if err := os.WriteFile("types_gen.go", buf.Bytes(), 0o666); err != nil {
   289  			return err
   290  		}
   291  		return fmt.Errorf("malformed source in types_gen.go:%v", err)
   292  	}
   293  	if err := os.WriteFile("types_gen.go", data, 0o666); err != nil {
   294  		return err
   295  	}
   296  	return nil
   297  }
   298  
   299  func generateFromFile(rootVal cue.Value) (fromFileInfo, error) {
   300  	allEncodings := append(allKeys[build.Encoding](rootVal, "all", "encodings"), "")
   301  	allInterpretations := append(allKeys[build.Interpretation](rootVal, "all", "interpretations"), "")
   302  	allForms := append(allKeys[build.Form](rootVal, "all", "forms"), "")
   303  	paramsStruct := newFromFileParamsStruct(
   304  		allEncodings,
   305  		allInterpretations,
   306  		allForms,
   307  	)
   308  	resultStruct := newFromFileResult(
   309  		allEncodings,
   310  		allInterpretations,
   311  		allForms,
   312  	)
   313  	var recordData []byte
   314  	for mode := range filetypes.NumModes {
   315  		for _, encoding := range allEncodings {
   316  			for _, interpretation := range allInterpretations {
   317  				for _, form := range allForms {
   318  					f := &build.File{
   319  						Encoding:       encoding,
   320  						Interpretation: interpretation,
   321  						Form:           form,
   322  					}
   323  					fi, err := fromFileOrig(rootVal, f, mode)
   324  					if err != nil {
   325  						continue
   326  					}
   327  					recordData = appendFromFileRecord(recordData, paramsStruct, resultStruct, mode, f, fi)
   328  				}
   329  			}
   330  		}
   331  	}
   332  	genstruct.SortRecords(recordData, paramsStruct.Size()+resultStruct.Size(), paramsStruct.Size())
   333  	if err := os.WriteFile("fromfile.dat", recordData, 0o666); err != nil {
   334  		return fromFileInfo{}, err
   335  	}
   336  
   337  	return fromFileInfo{
   338  		paramsStruct: paramsStruct,
   339  		resultStruct: resultStruct,
   340  	}, nil
   341  }
   342  
   343  func appendFromFileRecord(
   344  	data []byte,
   345  	paramsStruct *genFromFileParams,
   346  	resultStruct *genFromFileResult,
   347  	mode filetypes.Mode,
   348  	f *build.File,
   349  	fi *filetypes.FileInfo,
   350  ) []byte {
   351  	recordSize := paramsStruct.Size() + resultStruct.Size()
   352  	data = slices.Grow(data, recordSize)
   353  	data = data[:len(data)+recordSize]
   354  	record := data[len(data)-recordSize:]
   355  
   356  	// Write the key part of the record.
   357  	param := slices.Clip(record[:paramsStruct.Size()])
   358  	paramsStruct.Mode.Put(param, mode)
   359  	paramsStruct.Encoding.Put(param, f.Encoding)
   360  	paramsStruct.Interpretation.Put(param, f.Interpretation)
   361  	paramsStruct.Form.Put(param, f.Form)
   362  
   363  	result := slices.Clip(record[paramsStruct.Size():])
   364  	resultStruct.Encoding.Put(result, fi.Encoding)
   365  	resultStruct.Interpretation.Put(result, fi.Interpretation)
   366  	resultStruct.Form.Put(result, fi.Form)
   367  	resultStruct.Aspects.Put(result, fi.Aspects())
   368  	return data
   369  }
   370  
   371  func fromFileOrig(rootVal cue.Value, b *build.File, mode filetypes.Mode) (*filetypes.FileInfo, error) {
   372  	modeVal := lookup(rootVal, "modes", mode.String())
   373  	fileVal := lookup(modeVal, "FileInfo")
   374  	if b.Encoding == "" {
   375  		return nil, errors.Newf(token.NoPos, "no encoding specified")
   376  	}
   377  	fileVal = fileVal.FillPath(cue.MakePath(cue.Str("encoding")), b.Encoding)
   378  	if b.Interpretation != "" {
   379  		fileVal = fileVal.FillPath(cue.MakePath(cue.Str("interpretation")), b.Interpretation)
   380  	}
   381  	if b.Form != "" {
   382  		fileVal = fileVal.FillPath(cue.MakePath(cue.Str("form")), b.Form)
   383  	}
   384  	var errs errors.Error
   385  	var interpretation string
   386  	if b.Form != "" {
   387  		fileVal, errs = unifyWith(errs, fileVal, rootVal, "forms", string(b.Form))
   388  		if errs != nil {
   389  			return nil, errs
   390  		}
   391  		interpretation, _ = lookup(fileVal, "interpretation").String()
   392  		// may leave some encoding-dependent options open in data mode.
   393  	} else {
   394  		interpretation, _ = lookup(fileVal, "interpretation").String()
   395  		if interpretation != "" {
   396  			// always sets form=*schema
   397  			fileVal, errs = unifyWith(errs, fileVal, rootVal, "interpretations", interpretation)
   398  		}
   399  	}
   400  	if interpretation == "" {
   401  		encoding, err := lookup(fileVal, "encoding").String()
   402  		if err != nil {
   403  			return nil, err
   404  		}
   405  		fileVal, errs = unifyWith(errs, fileVal, modeVal, "encodings", encoding)
   406  	}
   407  
   408  	fi := &filetypes.FileInfo{}
   409  	if err := fileVal.Decode(fi); err != nil {
   410  		return nil, errors.Wrapf(err, token.NoPos, "could not parse arguments")
   411  	}
   412  	fi.Filename = b.Filename
   413  	return fi, errs
   414  }
   415  
   416  // unifyWith returns the equivalent of `v1 & v2[field][value]`.
   417  func unifyWith(errs errors.Error, v1, v2 cue.Value, field, value string) (cue.Value, errors.Error) {
   418  	v1 = v1.Unify(lookup(v2, field, value))
   419  	if err := v1.Err(); err != nil {
   420  		errs = errors.Append(errs,
   421  			errors.Newf(token.NoPos, "unknown %s %s", field, value))
   422  	}
   423  	return v1, errs
   424  }
   425  
   426  // cueValue holds a CUE value and an index that will be used
   427  // to distinguish that value in the generated source.
   428  type cueValue struct {
   429  	v     cue.Value
   430  	index int
   431  }
   432  
   433  // addCUELogic records the given CUE value as something that
   434  // we will need to generate Go logic for into byCUE,
   435  // and also adds any struct fields into keys.
   436  //
   437  // It returns the index recorded for the logic.
   438  func addCUELogic(v cue.Value, byCUE map[string]cueValue, keys map[string]bool) int {
   439  	data, err := format.Node(v.Syntax(cue.Raw()))
   440  	if err != nil {
   441  		panic(fmt.Errorf("cannot format CUE: %v", err))
   442  	}
   443  	for name := range structFields(v, cue.Optional(true)) {
   444  		keys[name] = true
   445  	}
   446  	src := string(data)
   447  	if v, ok := byCUE[src]; ok {
   448  		return v.index
   449  	}
   450  	index := len(byCUE)
   451  	byCUE[src] = cueValue{
   452  		v:     v,
   453  		index: index,
   454  	}
   455  	return index
   456  }
   457  
   458  func allCombinations(rootVal cue.Value, topLevelTags []string, tagInfo map[string]tagInfo) iter.Seq[fileResult] {
   459  	return func(yield func(fileResult) bool) {
   460  		var filenames []string
   461  		for ext := range structFields(lookup(rootVal, "modes", "input", "extensions")) {
   462  			filename := ext
   463  			if filename != "-" {
   464  				filename = "x" + filename
   465  			}
   466  			filenames = append(filenames, filename)
   467  		}
   468  		filenames = append(filenames, "x.unknown", "withoutextension")
   469  
   470  		for r := range tagCombinations(top, topLevelTags, tagInfo) {
   471  			for mode, modeVal := range structFields(lookup(rootVal, "modes")) {
   472  				fileVal := r.fileVal.Unify(lookup(modeVal, "FileInfo"))
   473  				for _, filename := range filenames {
   474  					r.mode = mode
   475  					r.filename = filename
   476  					r.file, r.fileVal, r.err = toFile1(modeVal, fileVal, filename)
   477  					r.subsidiaryBoolTags = lookup(r.fileVal, "boolTags")
   478  					r.subsidiaryTags = lookup(r.fileVal, "tags")
   479  					if !yield(r) {
   480  						return
   481  					}
   482  				}
   483  			}
   484  		}
   485  	}
   486  }
   487  
   488  func toFile1(modeVal, fileVal cue.Value, filename string) (*build.File, cue.Value, internal.ErrorKind) {
   489  	if !lookup(fileVal, "encoding").Exists() {
   490  		if ext := fileExt(filename); ext != "" {
   491  			extFile := lookup(modeVal, "extensions", ext)
   492  			if !extFile.Exists() {
   493  				return nil, cue.Value{}, internal.ErrUnknownFileExtension
   494  			}
   495  			fileVal = fileVal.Unify(extFile)
   496  		} else {
   497  			return nil, cue.Value{}, internal.ErrNoEncodingSpecified
   498  		}
   499  	}
   500  
   501  	// Note that the filename is only filled in the Go value, and not the CUE value.
   502  	// This makes no difference to the logic, but saves a non-trivial amount of evaluator work.
   503  	f := &build.File{Filename: filename}
   504  	if err := fileVal.Decode(f); err != nil {
   505  		return nil, cue.Value{}, internal.ErrCouldNotDetermineFileType
   506  	}
   507  	return f, fileVal, internal.ErrNoError
   508  }
   509  
   510  // allTags returns all tags that can be used and their types;
   511  // It also returns slices of the top level and subsidiary tag names.
   512  func allTags(rootVal cue.Value) (_ map[string]tagInfo, topLevel []string, subsid []string) {
   513  	tags := make(map[string]tagInfo)
   514  	add := func(name string, typ filetypes.TagType, v cue.Value) {
   515  		if other, ok := tags[name]; ok {
   516  			if typ != other.typ {
   517  				panic("tag redefinition")
   518  			}
   519  			return
   520  		}
   521  		info := tagInfo{
   522  			name:  name,
   523  			typ:   typ,
   524  			value: v,
   525  		}
   526  		if typ == filetypes.TagTopLevel {
   527  			topLevel = append(topLevel, name)
   528  		} else {
   529  			subsid = append(subsid, name)
   530  		}
   531  		tags[name] = info
   532  	}
   533  	addSubsidiary := func(v cue.Value) {
   534  		for tagName := range structFields(lookup(v, "boolTags")) {
   535  			add(tagName, filetypes.TagSubsidiaryBool, cue.Value{})
   536  		}
   537  		for tagName := range structFields(lookup(v, "tags")) {
   538  			add(tagName, filetypes.TagSubsidiaryString, cue.Value{})
   539  		}
   540  	}
   541  	for tagName, v := range structFields(lookup(rootVal, "tagInfo")) {
   542  		add(tagName, filetypes.TagTopLevel, v)
   543  		addSubsidiary(v)
   544  	}
   545  	addSubsidiary(lookup(rootVal, "interpretations"))
   546  	addSubsidiary(lookup(rootVal, "forms"))
   547  	sort.Strings(topLevel)
   548  	sort.Strings(subsid)
   549  	return tags, topLevel, subsid
   550  }
   551  
   552  func tagCombinations(initial cue.Value, topLevelTags []string, tagInfo map[string]tagInfo) iter.Seq[fileResult] {
   553  	return func(yield func(fileResult) bool) {
   554  		if len(topLevelTags) > 64 {
   555  			panic("too many tags")
   556  		}
   557  		type bitsValue struct {
   558  			bits uint64
   559  			v    cue.Value
   560  		}
   561  		evaluate := func(v bitsValue, tagIndex int, _ int) (bitsValue, bool) {
   562  			v.v = v.v.Unify(tagInfo[topLevelTags[tagIndex]].value)
   563  			v.bits |= 1 << tagIndex
   564  			return v, v.v.Validate() == nil
   565  		}
   566  
   567  		for v := range walkSpace(len(topLevelTags), 1, bitsValue{0, initial}, evaluate) {
   568  			if !yield(fileResult{
   569  				bits:    v.bits,
   570  				fileVal: v.v,
   571  				tags:    topLevelTags,
   572  			}) {
   573  				return
   574  			}
   575  		}
   576  	}
   577  }
   578  
   579  func (ts fileResult) String() string {
   580  	if ts.bits == 0 {
   581  		return "<none>"
   582  	}
   583  	var buf strings.Builder
   584  	for i, tag := range ts.tags {
   585  		if ts.bits&(1<<i) != 0 {
   586  			if buf.Len() > 0 {
   587  				buf.WriteByte('|')
   588  			}
   589  			buf.WriteString(tag)
   590  		}
   591  	}
   592  	return buf.String()
   593  }
   594  
   595  func (ts fileResult) Compare(ts1 fileResult) int {
   596  	return cmp.Compare(ts.String(), ts1.String())
   597  }
   598  
   599  // structFields returns an iterator over the names of all the fields
   600  // in v and their values.
   601  func structFields(v cue.Value, opts ...cue.Option) iter.Seq2[string, cue.Value] {
   602  	return func(yield func(string, cue.Value) bool) {
   603  		if !v.Exists() {
   604  			return
   605  		}
   606  		iter, err := v.Fields(opts...)
   607  		if err != nil {
   608  			return
   609  		}
   610  		for iter.Next() {
   611  			if !yield(iter.Selector().Unquoted(), iter.Value()) {
   612  				break
   613  			}
   614  		}
   615  	}
   616  }
   617  
   618  type genFromFileParams struct {
   619  	genstruct.Struct
   620  	Mode           genstruct.Accessor[filetypes.Mode]
   621  	Encoding       genstruct.Accessor[build.Encoding]
   622  	Interpretation genstruct.Accessor[build.Interpretation]
   623  	Form           genstruct.Accessor[build.Form]
   624  }
   625  
   626  type genFromFileResult struct {
   627  	genstruct.Struct
   628  	Encoding       genstruct.Accessor[build.Encoding]
   629  	Interpretation genstruct.Accessor[build.Interpretation]
   630  	Form           genstruct.Accessor[build.Form]
   631  	Aspects        genstruct.Accessor[internal.Aspects]
   632  }
   633  
   634  func newFromFileParamsStruct(
   635  	encodings []build.Encoding,
   636  	interpretations []build.Interpretation,
   637  	forms []build.Form,
   638  ) *genFromFileParams {
   639  	r := &genFromFileParams{}
   640  	r.Mode = genstruct.AddInt(&r.Struct, filetypes.NumModes, "Mode")
   641  	r.Encoding = genstruct.AddEnum(&r.Struct, encodings, "", "allEncodings", "build.Encoding", nil)
   642  	r.Interpretation = genstruct.AddEnum(&r.Struct, interpretations, "", "allInterpretations", "build.Interpretation", nil)
   643  	r.Form = genstruct.AddEnum(&r.Struct, forms, "", "allForms", "build.Form", nil)
   644  	return r
   645  }
   646  
   647  func newFromFileResult(
   648  	encodings []build.Encoding,
   649  	interpretations []build.Interpretation,
   650  	forms []build.Form,
   651  ) *genFromFileResult {
   652  	r := &genFromFileResult{}
   653  	r.Encoding = genstruct.AddEnum(&r.Struct, encodings, "", "allEncodings", "build.Encoding", nil)
   654  	r.Interpretation = genstruct.AddEnum(&r.Struct, interpretations, "", "allInterpretations", "build.Interpretation", nil)
   655  	r.Form = genstruct.AddEnum(&r.Struct, forms, "", "allForms", "build.Form", nil)
   656  	r.Aspects = genstruct.AddInt(&r.Struct, internal.AllAspects, "internal.Aspects")
   657  	return r
   658  }
   659  
   660  func newToFileParamsStruct(topLevelTags, fileExts []string) *genToFileParams {
   661  	r := &genToFileParams{}
   662  	r.Mode = genstruct.AddInt(&r.Struct, filetypes.NumModes, "Mode")
   663  	// Note: "" is a member of the set: we'll default to that if the extension isn't
   664  	// part of the known set.
   665  	r.FileExt = genstruct.AddEnum(&r.Struct, fileExts, ".unknown", "allFileExts", "string", nil)
   666  	r.Tags = genstruct.AddSet(&r.Struct, topLevelTags, "allTopLevelTags")
   667  	return r
   668  }
   669  
   670  type genToFileParams struct {
   671  	genstruct.Struct
   672  	Tags    genstruct.Accessor[iter.Seq[string]]
   673  	FileExt genstruct.Accessor[string]
   674  	Mode    genstruct.Accessor[filetypes.Mode]
   675  }
   676  
   677  type genToFileResult struct {
   678  	genstruct.Struct
   679  	Encoding       genstruct.Accessor[build.Encoding]
   680  	Interpretation genstruct.Accessor[build.Interpretation]
   681  	Form           genstruct.Accessor[build.Form]
   682  
   683  	// Note: the indexes below are one more than the actual index
   684  	// so that we can use the zero value to communicate "no tags".
   685  	SubsidiaryTagFuncIndex     genstruct.Accessor[int]
   686  	SubsidiaryBoolTagFuncIndex genstruct.Accessor[int]
   687  
   688  	Error genstruct.Accessor[internal.ErrorKind]
   689  }
   690  
   691  func newToFileResultStruct(
   692  	encodings []build.Encoding,
   693  	interpretations []build.Interpretation,
   694  	forms []build.Form,
   695  	subsidiaryBoolTagFuncCount int,
   696  	subsidiaryTagFuncCount int,
   697  ) *genToFileResult {
   698  	r := &genToFileResult{}
   699  	r.Encoding = genstruct.AddEnum(&r.Struct, encodings, "", "allEncodings", "build.Encoding", nil)
   700  	r.Interpretation = genstruct.AddEnum(&r.Struct, interpretations, "", "allInterpretations", "build.Interpretation", nil)
   701  	r.Form = genstruct.AddEnum(&r.Struct, forms, "", "allForms", "build.Form", nil)
   702  	r.Error = genstruct.AddInt(&r.Struct, internal.NumErrorKinds, "internal.ErrorKind")
   703  	r.SubsidiaryTagFuncIndex = genstruct.AddInt(&r.Struct, subsidiaryTagFuncCount, "int")
   704  	r.SubsidiaryBoolTagFuncIndex = genstruct.AddInt(&r.Struct, subsidiaryBoolTagFuncCount, "int")
   705  
   706  	return r
   707  }
   708  
   709  type dimspace[V any] struct {
   710  	evaluate      func(v V, dim, item int) (V, bool)
   711  	numDimensions int
   712  	numValues     int
   713  	yield         func(V) bool
   714  }
   715  
   716  // walkSpace explores the values that are possible to reach from the given initial
   717  // value within the given number of dimensions (numDimensions), where each point in space
   718  // can have the given number of possible item values (numItems).
   719  // It calls evaluate to derive further values as it walks the space, and
   720  // truncates the tree whereever evaluate returns false.
   721  //
   722  // Note that evaluate will always be called with arguments in the range [0, numDimensions)
   723  // and [0, numItems].
   724  //
   725  // Note also that this exploration relies on the property that evaluate is commutative;
   726  // that is, for a given point in the space, the result does not depend on the path
   727  // taken to reach that point.
   728  func walkSpace[V any](numDimensions, numValues int, initial V, evaluate func(v V, dim, item int) (V, bool)) iter.Seq[V] {
   729  	return func(yield func(V) bool) {
   730  		b := &dimspace[V]{
   731  			evaluate:      evaluate,
   732  			numDimensions: numDimensions,
   733  			numValues:     numValues,
   734  			yield:         yield,
   735  		}
   736  		b.walk(initial, 0)
   737  	}
   738  }
   739  
   740  func (b *dimspace[V]) walk(v V, maxDim int) bool {
   741  	if !b.yield(v) {
   742  		return false
   743  	}
   744  	for i := maxDim; i < b.numDimensions; i++ {
   745  		for j := range b.numValues {
   746  			if v1, ok := b.evaluate(v, i, j); ok {
   747  				if !b.walk(v1, i+1) {
   748  					return false
   749  				}
   750  			}
   751  		}
   752  	}
   753  	return true
   754  }
   755  
   756  func keys[K, V any](seq iter.Seq2[K, V]) iter.Seq[K] {
   757  	return func(yield func(K) bool) {
   758  		for k := range seq {
   759  			if !yield(k) {
   760  				return
   761  			}
   762  		}
   763  	}
   764  }
   765  
   766  func lookup(v cue.Value, elems ...string) cue.Value {
   767  	sels := make([]cue.Selector, len(elems))
   768  	for i := range elems {
   769  		sels[i] = cue.Str(elems[i])
   770  	}
   771  	return v.LookupPath(cue.MakePath(sels...))
   772  }
   773  
   774  func allKeys[T ~string](v cue.Value, elems ...string) []T {
   775  	return slices.Sorted(
   776  		seqMap(keys(structFields(lookup(v, elems...))), fromString[T]),
   777  	)
   778  }
   779  
   780  func fileExt(f string) string {
   781  	if f == "-" {
   782  		return "-"
   783  	}
   784  	e := filepath.Ext(f)
   785  	if e == "" || e == filepath.Base(f) {
   786  		return ""
   787  	}
   788  	return e
   789  }
   790  
   791  func seqMap[T1, T2 any](it iter.Seq[T1], f func(T1) T2) iter.Seq[T2] {
   792  	return func(yield func(T2) bool) {
   793  		for t := range it {
   794  			if !yield(f(t)) {
   795  				return
   796  			}
   797  		}
   798  	}
   799  }
   800  
   801  func fromString[T ~string](s string) T {
   802  	return T(s)
   803  }