Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: compile-time boolean assertions #34868

Open
mdempsky opened this issue Oct 13, 2019 · 12 comments
Open

proposal: compile-time boolean assertions #34868

mdempsky opened this issue Oct 13, 2019 · 12 comments
Labels
Proposal v2 A language change or incompatible library change
Milestone

Comments

@mdempsky
Copy link
Member

I propose adding compile-time boolean assertions to Go.

[I don't feel strongly about this proposal, but it seems pretty minimal; easy to implement; and to make some real world code somewhat easier to read/write. I've also not found any past discussion of this idea, so it seemed worth at least writing down even if rejected.]

Proposal

Concretely, I propose making these changes:

  1. Introduce a new "assert" package like:

    package assert
    
    type True bool
    
  2. Add a language rule that it's an error to have a constant of type assert.True but value false.

  3. (Optional) Add a language rule that it's an error to use assert.True except as the type of a constant.

Uses

There are somewhat common idioms of writing:

const _ = -uint(x - y) // assert x == y
const _ = uint(x - y)  // assert x >= y

But I at least find these awkward to reason about, even being very familiar with the details of how they work.

With this proposal, they could instead be written more clearly as:

import "assert"

const _ assert.True = x == y
const _ = assert.True(x >= y)

(Showing off two different ways to write const declarations using assert.True.)

Further, generalizing to boolean expressions allows us to easily use boolean operators to combine multiple tests. It also potentially allows static assertions involving non-integer constants (i.e., floats, complex, bools, and strings).

For example, package gc's sizeof_test.go could be rewritten as compile time asserts like:

const (
    ptrSize = unsafe.Sizeof((*int)(nil))
    funcSize = unsafe.Sizeof(Func{})

    _ = assert.True((ptrSize == 4 && funcSize == 116) || (ptrSize == 8 && funcSize == 208))
)

Backwards compatibility

assert.True doesn't exist today, so there's no code using it that we have to worry about.

Old tools unaware of the special semantics for assert.True (e.g., old compilers or tools using go/types) will continue working for old code. They'll also continue working correctly for new code that correctly use assert.True. The tools will, however, fail to detect failing assertions.

Related proposals

#9367 proposed allowing bool->int conversions, which be an alternative way of extending the current integer static assertions idiom to arbitrary boolean static assertions. However, it would still be somewhat awkward to read/write.

#30582 proposes an assertion to indicate unreachable code paths. Technically orthogonal to this one, but it might be worth ensuring they expose a consistent API to users.

C++11 added static_assert: https://en.cppreference.com/w/cpp/language/static_assert (Counter argument: C++11 has templates and constexpr, which make static_assert more broadly useful than assert.True would be.)

@mdempsky mdempsky added this to the Unplanned milestone Oct 13, 2019
@randall77
Copy link
Contributor

I'm not sure I see the need. What's wrong with init-time assertions? We can write them in regular Go code, no language changes needed. They don't fail at compile time, but they would fail unconditionally at binary startup time, so as long as you at least run a test, you'll find out.

@mdempsky
Copy link
Member Author

What's wrong with init-time assertions?

In general, I don't think there's anything wrong with them. But evidently people do like having compile-time assertions, as I've seen the compile-time assert pattern occur in a few places, despite its awkwardness.

As a minor nit, cmd/compile fails to dead-code eliminate

func init() {}

so adding init-level tests to your package currently bloats the resulting executables. (You can add it to the test package, but at that point it seems like you might as well just write it as a unit test.)

We should just fix that though (filed as #34869).

--

One alternative solution worth mentioning: go/types implements an assert builtin function for its unit tests. It requires the operand to be a true boolean constant, and errors otherwise.

This is probably an even simpler solution, since it only requires the language spec change to introduce a builtin. It doesn't require a new dummy package, or worrying about weird ways the assert.True type might be used.

@go101
Copy link

go101 commented Oct 13, 2019

We can use the var _ = map[bool]struct{}{false: struct{}{}, compileTimeCondition: struct{}{}} trick to assert any condition which can be evaluated at compile time now, though it is a little verbose. var _ = map[bool]int{false: 0, compileTimeCondition: 1} is a little shorter.

@andybons andybons modified the milestones: Unplanned, Proposal Oct 16, 2019
@ianlancetaylor
Copy link
Contributor

But evidently people do like having compile-time assertions, as I've seen the compile-time assert pattern occur in a few places, despite its awkwardness.

This approach is also a bit awkward, though.

We could perhaps do this via a vet check. Add a package assert with a function

func Assert(b bool) bool { return true }

This can be used as

const _ = assert.Assert(x == y)

From the language perspective, this is an inlined function that does nothing. But vet could look for this, and produce an error if the argument to assert.Assert were not true.

(As a completely different approach, we could add a builtin function assert(bool) that causes a compilation error if the argument is known to be false at compile time, or a run time error if the argument is not known at compile time but turns out to be false at run time. But that runs afoul of https://golang.org/doc/faq#assertions.)

@rsc rsc added the v2 A language change or incompatible library change label Oct 21, 2019
@rsc
Copy link
Contributor

rsc commented Oct 21, 2019

This seems like a Go2 change to me.
(It's not some tiny incremental thing. It's a real language change.)

@ianlancetaylor
Copy link
Contributor

The notion of adding a special assert.True is easy to specify, but it's hard to read. I agree that the current mechanisms that people use are harder to read. But if we are going to replace them, let's replace them with something that is easy to read, not merely less hard.

@griesemer suggests a different idea: if you write

func init() {
    if condition {
        panic("oh no!!") // or some other value
    }
}

then if the compiler can prove that the condition is true (if the condition is a constant expression), it reports an error oh no!! (or whatever) at compile time, rather than compiling the code to fail at run time. This would only happen in an init function, and would only happen for a non-nested if statement. This might be too complicated, but it would be easy to read.

@ianlancetaylor
Copy link
Contributor

@mdempsky Any thoughts on the idea above? Thanks.

@dotaheor
Copy link

Is the assure syntax a good solution for this need?

@bradfitz
Copy link
Contributor

Ping @mdempsky again.

@mdempsky
Copy link
Member Author

mdempsky commented Dec 10, 2019

@ianlancetaylor That seems like a reasonable counter proposal to me. Do you or @griesemer have any specific spec wording in mind?

I'd probably suggest something like:

Implementation restriction: If package initialization will always terminate due to panicking, a compiler may give an error instead of compiling the package.

That is, in addition to your example, I think this should be allowed to produce a compile-time error:

func init() { divBy(0) }
func divBy(x int) { _ = 1 / x }

As would:

var p *int
var u = *p

(In practice, cmd/compile probably wouldn't complain about this though, since we conservatively assume var p *int might have its initial value supplied by assembly.)

But this should not:

func init() {
    defer func() { recover() }()
    if true {
        panic("oh no")
    }
}

@beoran
Copy link

beoran commented Jan 8, 2020

Personally, I find the idea of using init() functions and panics to do static compile time checks very confusing. Panics have always been at run time, having to teach this idea to (new) Go language users that panics can also be compile time, sometimes, but not always, I expect to see a lot of puzzled faces.

My counter proposal is to provide a built in function, say, verify() that nominally takes one boolean as it's argument, and checks at compile time if the constant expression evaluates to true. If not, the compiler stops with a compile error, if so, the verify() function compiles away to nothing. If the expression cannot be evaluated at compile time, the compiler also stops with a compile error stating as much.

Like this, we avoid the traps of assert(), but add a feature to go that makes it easier to do these compile time checks and hence easier to write critical software in Go.

@griesemer
Copy link
Contributor

@beoran We might just as well call that function assert and only permit constant arguments.
(Just for reference: https://golang.org/doc/faq#assertions).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

10 participants