golang/go

go/ast, go/build/constraint: support for per-file Go versions using //go:build lines

rsc opened this issue · 16 comments

rsc commented

As part of the discussion during the acceptance of #57001, we decided the following.

  1. Starting in Go 1.21, we will interpret the go line in go.mod as the minimum Go version required. For example if Go 1.21 encounters a go.mod file that says go 1.22, it will refuse to build code in that module.
  2. To allow writing a module with a low minimum Go version but that can use newer features in //go:build-tagged files, we will recognize //go:build constraints that require a newer Go version than the go.mod go line and allow newer Go features in those files. We are also going to allow a //go:build constraint to declare an older version, which will lock out newer Go features that would normally be allowed by the go.mod go version. For compatibility reasons, that “downgrading” can only apply for go.mod files saying go 1.21 or later.

To implement (2), we need a small amount of new API: go/build/constraint needs to export code to compute the implied minimum Go version from a constraint.Expr, and go/ast needs to expose the minimum Go version as a new field in ast.File.

This proposal is about that new API. I propose:

package constraint

// GoVersion returns the minimum Go version implied by a given build expression.
// If the expression can be satisfied without any Go version tags, GoVersion returns an empty string.
//
// For example:
//
//	GoVersion(linux && go1.22) = "go1.22"
//	GoVersion((linux && go1.22) || (windows && go1.20)) = "go1.20" => go1.20
//	GoVersion(linux) = ""
//	GoVersion(linux || (windows && go1.22)) = ""
//
// GoVersion assumes that any tag or negated tag may independently be true,
// so that its analysis can be purely structural, without SAT solving.
// “Impossible” subexpressions may therefore affect the result.
//
// For example:
//
//	GoVersion((linux && !linux && go1.20) || go1.21) = "go1.20"
func GoVersion(x Expr) string

And

package ast

type File struct {
	...
	GoVersion          string          // minimum Go version required by //go:build directives
}

go/parser would populate ast.File.GoVersion, and go/types would use that information to decide whether specific features are allowed in certain files.

Change https://go.dev/cl/476275 mentions this issue: go/build/constraint: add GoVersion

Change https://go.dev/cl/476276 mentions this issue: go/ast: add File.GoVersion

Change https://go.dev/cl/476277 mentions this issue: go/parser: report //go:build-derived Go version in ast.File.GoVersion

Change https://go.dev/cl/476278 mentions this issue: go/types, cmd/compile/internal/types2: use per-file Go version

It seems like this does slightly interfere with "all developers working on a project will agree on the toolchain version"; if I am using 1.22, someone else is using 1.23, the app under development has a 1.21 go.mod minimum version but contains files with 1.23 build tags, the two of us will see different builds of the app. I still want this change, though.

rsc commented

All developers still get the same, reproducible build. That build just agrees on which files use which Go dialects. I don't think this conflicts with that.

rsc commented

I should add a ! example; GoVersion(!go1.22) = "".

rsc commented

This proposal has been added to the active column of the proposals project
and will now be reviewed at the weekly proposal review meetings.
— rsc for the proposal review group

Is there a comment in the prior issue which summarizes the use case for having an older toolchain version for an individual file then the minimum go version in the module? It seems like a strange case to me.

rsc commented

Is there a comment in the prior issue which summarizes the use case for having an older toolchain version for an individual file then the minimum go version in the module? It seems like a strange case to me.

If you are updating and know that the older file needs older semantics, you can set the default for the module (to cover new files created there) and then special-case the few files you need older semantics in.

rsc commented

Based on the discussion above, this proposal seems like a likely accept.
— rsc for the proposal review group

rsc commented

No change in consensus, so accepted. 🎉
This issue now tracks the work of implementing the proposal.
— rsc for the proposal review group

Change https://go.dev/cl/499415 mentions this issue: doc/go1.21: mention directive handling in go/{ast,build}

I think this was implemented in Go 1.21. @rsc, is there anything more to do for it?

rsc commented

Done!