Why is type coersion not transitive?

The documentation say that *T can be cast to *[1]T, and *[N]T to []T. This means that this obviously works:

var x: i32 = 1234;
var y: *[1]i32 = &x;
var z: []i32 = y;

Removing the second step breaks the compilation:

var x: i32 = 1234;
var z: []i32 = &x;
// error: expected type '[]i32', found '*i32'

Is this an explicit decision? If yes, what’s the rationale behind it?

2 Likes

This question actually came up in a discussion with a core team member in the Zig discord just a week ago (though to be precise it was about [*]T, not []T, but they are basically the same in the context of this question) and the rationale that we were given is that it’s more likely that a coercion from *T[]T hides an unintentional bug than the coercion from *T*[1]T.

Extrapolating on this, consider

fn gimmeOne(x: *const [1]T) void { ... }
fn gimmeMany(x: []const T) void { ... }

const x: T = ...;
gimmeOne(&x);
gimmeMany(&x);

In the first call, gimmeOne(&x), it is very unlikely that I’ve made a mistake that is indicative of a bug. *T and *[1]T both have the exact same representation in memory and mean the same thing (a pointer to a single thing), so the intent is already clear.

In the second call, gimmeMany(&x), it’s more ambiguous. Did I really mean to pass &x here, or did I mistake it for a different parameter or a different function altogether? *T and []T mean different things, so the likelihood of an unintentional bug is higher than the first call. If passing &x as a slice with a length of 1 was intentional, I should use (&x)[0..1] to make my intent clear to the reader.

11 Likes

Thank you for the explanation.