Runtime errors should be compile-time errors?

Hello! I had a question regarding the following two code blocks (see link attached if you want to run):

Example #1:

module foo {
  proc main() {
    var y: [1..5] real = [1, 2, 3, 4];
    writeln(y);
  }

}

Compiles but raises this runtime error:

src/main.chpl:10: error: halt reached - assigning between arrays of different shapes in dimension 0: 5 vs. 4

Example #2

module foo {

  proc bar(const ref x: [?D] real, const ref y: [D] real): [D] real {
    return x + y;
  }

  proc main() {
    var x: [1..5] real = [1, 2, 3, 4, 5];
    var z: [1..4] real = [1, 2, 3, 4];
    writeln(bar(x, z));
  }

}

Compiles but raises the following runtime error:

src/main.chpl:3: error: halt reached - Domain mismatch passing array argument:Formal domain is: {1..5}Actual domain is: {1..4}

These were compiled with chpl --warnings -o build/main src/main.chpl. I’m curious why don’t they raise compile time errors? Is there a more elegant approach to achieve that kind of check at compile time?

Hi Jared —

Chapel's array sizes generally don't need to be known at compile-time, and for many typical programs aren't, which is one aspect of why we haven't invested effort in doing compile-time size compatibility checks for cases like the ones you illustrate here, where the low and high bounds both happen to be compile-time known values (param ints, essentially). Major arrays in Chapel are often sized by 1..n or the like, where n is a config const, for example.

We aspire to expand Chapel's param machinery (essentially the set of things it can reason about at compile-time) to include ranges (like 1..5 in your example), domains (the 1..5 once it's used to define an array), and arrays (like `[1, 2, 3, 4] in your example), but have not had the opportunity to prioritize this as of yet. Issues like [Feature Request]: Include param valued ranges · Issue #25583 · chapel-lang/chapel · GitHub and feature request: param arrays, records, tuples, ... · Issue #23431 · chapel-lang/chapel · GitHub capture this desire and touch on some of the effort required to achieving it.

In contrast to arrays, the size of Chapel tuples must be known at compile-time, so for small collections of values, like the ones in your example, switching to tuples can provide those types of compile-time guarantees, while also resulting in a more efficient execution-time implementation than the general-purpose array types (with some tradeoffs like "must (currently) use 0-based indexing", "cannot be strided", "cannot be multidimensional (but can be nested)").

As an example, here's a rewrite of your first example to use tuples that results in a compiler-time message, and here's a similar rewrite for the second.

Please feel encouraged to thumbs-up or comment on the issues above—or create your own—if you would like to advocate for prioritizing compile-time checks for param-sized arrays.

Please let us know if you have additional questions here,
-Brad

1 Like

Hello Brad,

Thank you this is a great example using tuples, and for fixed size arrays this is a very solid alternative. As someone who does a lot of python at work, I’m in favor of any array checks the compiler can do.

I think in my above code, I’m imagining a perceived issue here that wouldn’t be any different in another language. For instance, if I wrote a C library function that took in 2 equal length arrays and linked it into another C application, an error like that wouldn’t be caught until runtime either. Maybe I’m still just getting used to Chapel - in Python you can type hint all you want and it won’t make a difference until runtime. In C++/fortran it’s mandatory (for types, not array sizes as discussed above). In Chapel, type check errors will flag the compiler, but only as much as a developer disciplines themselves to do it.

I didn’t see a flag under chpl -h , but is there a “strict” mode in Chapel that warns the developer of missing/mismatched types? Maybe that’d get in the way of some generics/first-class functions handling, which I definitely wouldn’t want to go away (I love the lack of boilerplate in Chapel compared to say, C++).

1 Like

Hi Jared —

You're very welcome! Focusing on this sentence:

In Chapel, type check errors will flag the compiler, but only as much as a developer disciplines themselves to do it.

note that Chapel does do compile-time type checking, but it's important to be clear about what that encompasses:

For examples like yours with array size mismatches, an array's size is not part of its type in Chapel (nor are its indices), so the type-checking performed doesn't protect against this case. We could still invest in generating compiler errors for simple cases like yours that used param indices, but it wouldn't technically be type-checking—more like a helpful value-check that would save time and reduce frustration (both very worthwhile things).

In contrast, things like the number of dimensions in an array or its element type are part of its type, and the compiler guards against mistakes made there. For example, the following declarations would generate errors, the first because of a rank mismatch (2D vs. 1D arrays), and the second because real values can't be assigned to ints:

var A: [1..2, 1..2] real = [1, 2, 3, 4];
var B: [1..4] int = [1.0, 2.0, 3.0, 4.0];

Note that it's frequently useful that an array's indices are not part of its type because it permits patterns like the following that assign between expressions with different indices:

var C: [1..4] real = SomeOtherArray[lo..#4];
var D: [1..4] int = [1, 2, 3, 4];   // note that the RHS array defaults to indices`0..3`

Similarly, while it might seem attractive to imagine an array's size being part of its type, practically speaking, it could cause issues or code explosions when passing arrays to procedures. E.g., imagine specializing a procedure that printed out 1D arrays for every size that was passed into it in practice.

Popping back to this part of your statement:

only as much as a developer disciplines themselves

I wanted to make sure it's clear that whether a declaration includes an explicit type or not, the compiler will still give it a well-defined static type, and will still do type-checking against it. Thus, whether you declare your variables using:

var A = [1, 2, 3, 4],
    t = (0, 1, 2),
    pi = 3.1415;

or:

var A: [0..3] int = [1, 2, 3, 4],
    t: 3*int = (0, 1, 2),
    pi: real = 3.1415;

the types and type-checking performed on both versions will be the same.

Of couse what's not done is to check that the initialization expression and the variable's type match the user's expectation. So for example, if I were thinking "I'm going to declare a boolean and a 2D array" and wrote the following:

var b = flag1 + flag2,  // where flag1 and flag2 are bools
    A2 = [[1, 2], [3, 4]];

then the compiler would do what I wrote, not what I thought I was writing, and make b an int (adding bools yields integers; probably I should've used | if I thought of + as "or"), while making A2 a nested array-of-arrays.

In contrast, if I were to assert the types I was expecting by specifying them:

var b: bool = flag1 + flag2,
    A2: [0..1, 0..1] int = [[1, 2], [3, 4]];

then the compiler would point out the disconnect between what I was expecting to get and what I actually wrote, which might prompt me to rewrite it as:

var b: bool = flag1 | flag2,
    A2: [0..1, 0..1] int = [1, 2; 3, 4];

though of course I could've written it like this from the start and ended up in the same place:

var b = flag1 | flag2,
    A2 = [1, 2; 3, 4];

(note that similar situations could come up when passing an expression to a formal argument with or without a type constraint on it).

In this sense, you're correct that developer discipline can head off mistakes—typically user mistakes or mis-understandings of how the language works. But I wouldn't want you to think that relying on type inference somehow makes the compiler skip type checking or be more relaxed about an identifiers type—that's the same either way.

I love the lack of boilerplate in Chapel compared to say, C++

Thanks for saying so. I'm obviously biased, but I completely agree!

Have a good weekend,
-Brad

Thank you for your detailed reply here Brad. You make a good distinction about types vs sizes, and I agree it wouldn’t make sense to include the size with the type. I’m happy to hear that regardless of explicitly including types, the compiler-checks occur regardless. Being explicit in the syntax hardly adds “noise” in this language, and as you pointed out there’s real benefit to doing so.

1 Like