The cue.mod/module.cue file has a mandatory language.version field that sets the language version for a given module. But what does it actually do? This page explores that question.

Here is an example module.cue file:

cue.mod/module.cue
module: "cue.example"
language: version: "v0.9.2"

At the time of writing, the latest CUE release is v0.9.2, so we can see that the language version is closely related to the CUE toolchain (cue command or API) version.

The language.version field is required for two main reasons. The information that it encodes:

  • protects you from using CUE modules that rely on language features that aren’t implemented in the version of CUE you have installed;
  • allows the CUE language developers to make changes to the language that aren’t backwards compatible

Let us consider each of these separately.

Use of future-versioned code

When the CUE evaluator comes across a module that declares a language version greater than that of the evaluator itself, it produces an error. This is because the evaluator cannot know what semantics apply to the newer version. Even if the module does not actually make use of any newer language features, the older version of the evaluator cannot know that’s the case, so the conservative choice is to produce an error. This would happen, for example, if you used v0.9.0 of the cue command to evaluate a module that declares a language.version field containing v0.10.0.

As a historical counterpoint, the Go language took a different approach initially: when it came across a newer version, it tried to compile the code anyway, optimistically assuming that it might be OK. On failure, it produced an error message saying that the syntax error “might be due to use of a newer language feature”. CUE does not use that approach for a few reasons:

  • the resulting errors are obscure and may well not pinpoint the actual reason for failure, which may not be anything to do with the language version
  • the language change might be subtle enough that it doesn’t actually result in an error
  • Go deliberately moved away from that approach

Making backwards incompatible changes

It’s important to be able to move forward with changes to the language, adding new features, and sometimes deprecating or even removing features.

One example is the way that aliases are defined. Originally, an alias was defined using this syntax:

x = 5 // Don't use this old alias syntax!

but in more recent CUE versions, this has been changed to:

let x = 5

If the language were never to be changed in a backwards incompatible way, the original syntax would need to be supported forever, but that would preclude the possibility of moving forward and perhaps repurposing the x=5 syntax for other things.

However, having the language version inside the module.cue file provides the CUE language designers with the freedom to both support the old syntax and to repurpose it for other things by interpreting the files in a module in the knowledge of the language version declared for that module.

Specifically the evaluator can see that a module declares an older version and apply the older syntax or semantics to the files in that module.

This also applies to changes to the module.cue schema itself. Although the CUE developers will strive to avoid making backwards incompatible changes, they cannot necessarily foresee future requirements, so the language version also allows us to make backwards incompatible changes to the module.cue file syntax.

How CUE behaves

So how does CUE actually behave when it evaluates modules?

Given an evaluator version E and a module version V:

  • if V is after E, the evaluator will fail with an error (module is too new)
  • if V is before E, the evaluator will interpret the module as if it were evaluator version V

In time, it is likely that support for some old features will be removed, with plenty of notice given. In that case, if V is a long way before E, the evaluator may fail instead of interpreting the module as V.