CUE Package ManagementDependency ManagementRelease Engineering 5 min read

CUE v0.18.0-rc.2 Introduces Module Upgrades and Language Versioning

The latest CUE release candidate simplifies dependency management with a new upgrade command, introduces per-file language versioning, and includes significant evaluator performance improvements.

CUE v0.18.0-rc.2, a release candidate for the configuration language, was published on October 26. It adds a cue mod upgrade command and a per-file language-version attribute. It also makes the evaluator much faster on one common pattern and rejects some function configurations that earlier builds accepted.

Language versions outside modules

A CUE file that belongs to no module can now declare the language version it targets with a @version attribute, for example @version(v0.17.0). The attribute goes ahead of any other file attribute and the package clause. A reproducer or standalone file written for an older version can then run as written, without a module being created for it. The attribute is an error in a file inside a module that declares a language version, because the module file decides that version. When cue fix enables an experiment in such a file, it adds the attribute and records the version the file was migrated at.

A new --language-version flag does the same job from the command line. It works on commands that read CUE outside a module, including cue eval, cue export, cue vet, cue fmt, cue fix, cue import and cue get go, and it also applies to standard input. It is only a fallback: a module's declared version and a file's own @version attribute both take precedence. Output from cue import, cue get go and cue get crd targets the chosen version. Go users get the equivalent cue/load.Config.LanguageVersion field, plus cue/build.Instance.TargetLanguageVersion, which reports the version that cue.Value.Syntax targets for an instance.

Module upgrades and the functions experiment

The new cue mod upgrade command moves a module to a new language version. It rewrites every CUE file in the module, including those under cue.mod, and then records the version in cue.mod/module.cue. Files are rewritten without being loaded, so a package with an unresolvable import is still converted. If any file cannot be converted, the upgrade stops before writing anything unless --force is given. cue fix run without package arguments now fixes every file in the module from any directory inside it. The explicitopen rewrite also no longer closes values in several cases where the original left them open.

The release carries one change flagged as potentially breaking for the functions experiment. It now follows a revised proposal on function contracts. An implementation unified with a function type must accept every argument the type admits, and callbacks are checked where they are passed. Two function literals with different bodies no longer compose when unified, which undoes a change announced in v0.18.0-alpha.3. Some configurations that evaluated before are now rejected, and the proposal lists them and how to fix them. The language server now understands these function literals. Go-to-definition, find references, rename and completion work inside them, and hovering a parameter shows its constraint.

Evaluator and performance

A reference unified with a definition, or with a disjunction of definitions, is now shared with the value it refers to instead of being re-evaluated under every path. The release notes give deps: [...#Node] with #Node: #Task | #Group as an example. A dependency graph typed this way is evaluated once per node rather than exponentially in its depth, whatever order the fields are declared in. Exporting a struct that holds many unifications of empty or single-field structs no longer needs memory exponential in their number.

The release also fixes a case where validators were skipped. The != and =~ bounds and builtin validators on a field in an inline struct, or in a let read through a reference such as an interpolation, were silently dropped before. They are now checked. That surfaces conflicts earlier releases accepted, such as a let inside a definition that sets an integer on a field declared as a float with a default.

In cue cmd, memory use no longer grows with the number of tasks that return a result, because earlier configurations are released once the tasks depending on them finish. For a command running 40 such tasks, peak memory falls from about 500MB to about 125MB. cue cmd also now reports errors from a command that declares no tasks, alongside the "no tasks found" message.

Encodings, Go API and packaging

The YAML decoder no longer swallows the key after a sequence that ends with an empty entry and is not indented relative to its key. The JSON Schema decoder no longer keeps a repeated allOf conjunct twice when merging. The CUE returned by Extract and ExtractCRDs in encoding/jsonschema, and by Extract in encoding/protobuf, now follows the style of cue/format.Simplify. Labels are unquoted where possible, so Go API users see the same output that cue import prints.

For programs that embed CUE, the text/template builtin and cue/stats looked up methods by name. That stops the Go linker from discarding unused exported methods anywhere in the binary, and one reported binary was 66MiB instead of 25MiB. cue/stats no longer does this. Building with the new cuenotemplate tag leaves text/template out of the standard library so the linker can discard unused methods again, at the cost that CUE code importing text/template fails.

Homebrew users are affected too. The cue-lang/tap tap is retired in favor of Homebrew's own formula, installed with brew install cue. Existing tap installations move over automatically on brew update.