Why can't a [N]T value be coerced into [N:c]T?

 pub fn main() void {
    var hello: [5]u8 = .{'H', 'e', 'l', 'l', 'o'};
    var world: [5:0]u8 = .{'w', 'o', 'r', 'l', 'd'};

    hello = world; // okay
    // world = hello; // error, but why?
    _ = &world;
} 

Because hello doesn’t have a 0 at the end.

Maybe the title is bad. More precisely, I mean, why isn’t that assignment legal. After all the tail sentinel is not overwritten.

If we for example have the strings in memory like this

Addr: 0 1 2 3 4 5 6 7 8 9 10
Val:  H e l l o w o r l d 0

IIUC we have something like hello = .{ .ptr = 0, .len = 5 } and world = .{ .ptr = 5 }, where world doesn’t store length and requires null termination.
When doing hello = world that is fine, and just sets ptr to be the same, and len to what length we have before null termination, i.e. hello = .{ .ptr = 5, .len = 5 }.
But doing world = hello is not valid as we can’t guarantee that hello has a null termination after what it points to in memory, so just moving over the pointer value is not valid.

On the phone and haven’t verified this, so might be completely wrong. But this is how I have understood it previously at least.

It think your explanations are valid for the coercion relations between []u8 and [:0]u8, but not for [N]u8 and [N:0]u8.

In Zig, the general rule is that values of a narrower type can be coerced into a wider type. For array values, however, this rule is inverted.

const print = @import("std").debug.print;

pub fn main() void {
    const x: u8 = 123;
    _ = @as(u32, x); // to a wider type
    _ = @as(?u8, x); // to a wider type
    
    const world: [5:0]u8 = .{'w', 'o', 'r', 'l', 'd'};
    _ = @as([5]u8, world); // to a narrower type, and okay
    
    const hello: [5]u8 = .{'H', 'e', 'l', 'l', 'o'};
    //_ = @as([5:0]u8, hello); // to a wider type, but error
    _ = &hello;
}