Compile-time assertions

Consider the following code:

struct Matrix {

   let rows : Int
   let cols : Int 
   var data : [Double]

   static func *(lhs: Matrix, rhs: Matrix) -> Matrix {
      assert(lhs.rows == rhs.cols)
      //allocate result, call dgemm, return result 
   }

}

In general, it is impossible to throw this assertion at compile time. Scenarios with these are the reason why there is some discussion about dependent types so that you can declare a Matrix<4,2> or something (which unfortunately would make type inference undecidable [which it already is, but this would make it even worse]). However, if you do have a dynamic scanerio (e.g. you load a matrix from some file and you don't know the size in advance), you may want to have a plain Matrix - along with multiplication operators not only between dynamic matrices, but also with Matrix<42,1337> - boilerplate around the corner!

I thought: maybe there's a workaround? Say, I write:

let a = Matrix(rows: 2, cols: 4, data: [...])
let b = Matrix(rows: 3, cols: 5, data: [...])
let c = a*b

Then, rows and cols of both matrices are literals and therefore known at compile time. Since the compiler knows (from reading the initializer code) that the arguments rows and cols map to the matrix properties of the same name and since a and b are let constants, it should be feasible to infer the values of those properties in *. Note that in order to evaluate the assertion in *, the compiler does not need to know data - it can just mark it as a dynamic property and mark everything that depends on it as dynamic. But since the assertion only depends on expressions that the compiler could mark as a "constexpr", it could throw this assertion in our case (since 4 isn't 3). All it would need to do is to automatically propagate statically known values (initialized with literals or static let or global let, where the latter two may depend on side-effects and therefore be environment dependent) through everything that guarantees exclusive access (no messing around with the value by other threads or something).

The elegance of this approach is that developers don't need to learn anything new. They only get the benefit of not being able to even run some incorrect code. However, I have no idea how feasible it is and how much it would impact compile times. Another aspect that I don't know about: what would be the appropriate place to throw the assertion? If the compiler throws it inside the implementation of *, you have no idea where it comes from, so better throw it at the callsite. But the callsite may still be a couple of steps away from the place where the constexprs get their value.

Thoughts?

Edit:
I get that this may be source-breaking if the error is thrown as an error rather than as a warning; however, why not break apps where the compiler can prove they'll crash anyway? The only reason why you would deliberately crash your app with something like let foo : Int? = nil; print(foo!) is because you have an irrational aversion against typing fatalerror.

There's an on-going discussion about compile-time constant expressions which is inherently related to assertions: Compile-Time Constant Expressions for Swift. Check the Static assertions section of that proposal.

Furthermore, you are indirectly looking for generic parameters having instance values or literal types. There has been discussion in this field, you can look at the Vector manifesto and the Generic value parameters section of the Generic Manifesto.

2 Likes

Yeah, I was thinking about a more implicit approach that unifies static and dynamic assertions where runtime assertions can be thrown at compile time in some cases (where the compiler can figure out the concrete boolean value) and at runtime in other cases to avoid boilerplate. Enabling the user to ensure compile-time execution (rather than hoping that the compiler is smart enough) is a good thing as well.

Terms of Service

Privacy Policy

Cookie Policy