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 · 10 comments
Open

proposal: compile-time boolean assertions #34868

mdempsky opened this issue Oct 13, 2019 · 10 comments
Labels
Milestone

Comments

@mdempsky
Copy link
Member

@mdempsky mdempsky commented Oct 13, 2019

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 the Proposal label Oct 13, 2019
@mdempsky mdempsky added this to the Unplanned milestone Oct 13, 2019
@randall77

This comment has been minimized.

Copy link
Contributor

@randall77 randall77 commented Oct 13, 2019

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

This comment has been minimized.

Copy link
Member Author

@mdempsky mdempsky commented Oct 13, 2019

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

This comment has been minimized.

Copy link

@go101 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

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Oct 16, 2019

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

This comment has been minimized.

Copy link
Contributor

@rsc 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

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Nov 5, 2019

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

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Nov 26, 2019

@mdempsky Any thoughts on the idea above? Thanks.

@dotaheor

This comment has been minimized.

Copy link

@dotaheor dotaheor commented Nov 27, 2019

Is the assure syntax a good solution for this need?

@bradfitz

This comment has been minimized.

Copy link
Member

@bradfitz bradfitz commented Dec 10, 2019

Ping @mdempsky again.

@mdempsky

This comment has been minimized.

Copy link
Member Author

@mdempsky 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")
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
8 participants
You can’t perform that action at this time.