From 6e053dbf04a224d5aa73dd976c994ec68dd5a7cf Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Tue, 20 Oct 2026 00:21:56 -0700 Subject: [PATCH 1/6] Sema: include sentinel in the allocated result of array concatenation --- src/Sema.zig | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Sema.zig b/src/Sema.zig index 208bde58a95cd3c8b0dbf5b5ce6d68d67804e092..733703bffe2328f314bd176de8bcbec8ba3e38cf 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -13560,13 +13560,23 @@ fn zirArrayCat(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai const lhs_len = try sema.usizeCast(block, lhs_src, lhs_info.len); const rhs_len = try sema.usizeCast(block, rhs_src, rhs_info.len); - const result_len = std.math.add(usize, lhs_len, rhs_len) catch |err| switch (err) { - error.Overflow => return sema.fail( + const result_len, const result_len_with_sent = blk: { + if (std.math.add(usize, lhs_len, rhs_len)) |result_len| { + const sent_len = @intFromBool(res_sent_val != null); + if (std.math.add(usize, result_len, sent_len)) |with_sent| { + break :blk .{ result_len, with_sent }; + } else |err| switch (err) { + error.Overflow => {}, + } + } else |err| switch (err) { + error.Overflow => {}, + } + return sema.fail( block, src, "concatenating arrays of length {d} and {d} produces an array too large for this compiler implementation to handle", .{ lhs_len, rhs_len }, - ), + ); }; const result_ty = try pt.arrayType(.{ @@ -13604,7 +13614,7 @@ fn zirArrayCat(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai else rhs_val; - const element_vals = try sema.arena.alloc(InternPool.Index, result_len); + const element_vals = try sema.arena.alloc(InternPool.Index, result_len_with_sent); var elem_i: u32 = 0; while (elem_i < lhs_len) : (elem_i += 1) { const lhs_elem_i = elem_i; @@ -13632,6 +13642,9 @@ fn zirArrayCat(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai const coerced_elem_val = sema.resolveValue(coerced_elem_val_inst).?; element_vals[elem_i] = coerced_elem_val.toIntern(); } + if (res_sent_val) |v| { + element_vals[result_len] = v.toIntern(); + } return sema.addConstantMaybeRef( try pt.aggregateValue(result_ty, element_vals), ptr_addrspace != null, -- 2.54.0 From 0b737050465cfd9a9a497ffed60517d52fd2e0db Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Tue, 20 Oct 2026 00:53:04 -0700 Subject: [PATCH 2/6] Sema: include sentinel in allocated result of unflattenArray --- src/Sema/comptime_ptr_access.zig | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Sema/comptime_ptr_access.zig b/src/Sema/comptime_ptr_access.zig index 96d407f9ffb2fe271fa4f1325343928bbb700d97..d421af53adb7046cf08fcb9ec0a5df3382a3e8a0 100644 --- a/src/Sema/comptime_ptr_access.zig +++ b/src/Sema/comptime_ptr_access.zig @@ -1031,13 +1031,12 @@ fn unflattenArray( } const elem_ty = ty.childType(zcu); - const buf = try arena.alloc(InternPool.Index, @intCast(ty.arrayLen(zcu))); + const buf = try arena.alloc(InternPool.Index, @intCast(ty.arrayLenIncludingSentinel(zcu))); for (buf) |*elem| { elem.* = (try unflattenArray(sema, elem_ty, elems, next_idx)).toIntern(); } - if (ty.sentinel(zcu) != null) { - // TODO: validate sentinel - _ = try unflattenArray(sema, elem_ty, elems, next_idx); + if (ty.sentinel(zcu)) |sent| { + assert(sent.toIntern() == buf[buf.len - 1]); } return pt.aggregateValue(ty, buf); } -- 2.54.0 From 30b1ec85dd5d11a7dd767275ae3e65f17035d2f3 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Tue, 20 Oct 2026 01:27:43 -0700 Subject: [PATCH 3/6] Sema: include sentinel in dest elems when coercing tuple to array --- src/Sema.zig | 3 ++- test/behavior/array.zig | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Sema.zig b/src/Sema.zig index 733703bffe2328f314bd176de8bcbec8ba3e38cf..a83f9ec67af8088d413abdce43e246b2ccb0deff 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -30670,6 +30670,7 @@ fn coerceTupleToArray( const inst_ty = sema.typeOf(inst); const inst_len = inst_ty.arrayLen(zcu); const dest_len = dest_ty.arrayLen(zcu); + const dest_len_including_sent = dest_ty.arrayLenIncludingSentinel(zcu); if (dest_len != inst_len) { const msg = msg: { @@ -30682,7 +30683,7 @@ fn coerceTupleToArray( return sema.failWithOwnedErrorMsg(block, msg); } - const dest_elems = try sema.usizeCast(block, dest_ty_src, dest_len); + const dest_elems = try sema.usizeCast(block, dest_ty_src, dest_len_including_sent); const element_vals = try sema.arena.alloc(InternPool.Index, dest_elems); const element_refs = try sema.arena.alloc(Air.Inst.Ref, dest_elems); const dest_elem_ty = dest_ty.childType(zcu); diff --git a/test/behavior/array.zig b/test/behavior/array.zig index 10cbdd6f731658795f030c4167c6d204079767bc..574d0f2a007134d1bccc5bce2a300da0abd43fc7 100644 --- a/test/behavior/array.zig +++ b/test/behavior/array.zig @@ -591,6 +591,9 @@ test "tuple to array handles sentinel" { var b: [3:0]u8 = a; }; try expect(S.b[0] == 1); + try expect(S.b[1] == 2); + try expect(S.b[2] == 3); + try expect(S.b[3] == 0); } test "array init of container level array variable" { -- 2.54.0 From 3c0e8b34d76d7c081a5d8b1f2d060fe6735e8d12 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Tue, 20 Oct 2026 01:48:32 -0700 Subject: [PATCH 4/6] Sema: include sentinel in allocated result of array-like coercion to array --- src/Sema.zig | 11 +++++++++-- test/behavior/array.zig | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Sema.zig b/src/Sema.zig index a83f9ec67af8088d413abdce43e246b2ccb0deff..bb009852de44273bfa5ca94605bcacfb1dfd5a8f 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -30628,11 +30628,18 @@ fn coerceArrayLike( return block.addTyOp(.array_to_vector, dest_ty, inst); } - const element_vals = try sema.arena.alloc(InternPool.Index, dest_len); - const element_refs = try sema.arena.alloc(Air.Inst.Ref, dest_len); + const dest_len_including_sent = try sema.usizeCast(block, dest_ty_src, dest_ty.arrayLenIncludingSentinel(zcu)); + const element_vals = try sema.arena.alloc(InternPool.Index, dest_len_including_sent); + const element_refs = try sema.arena.alloc(Air.Inst.Ref, dest_len_including_sent); var runtime_src: ?LazySrcLoc = null; for (element_vals, element_refs, 0..) |*val, *ref, i| { + if (i == dest_len) { + const sentinel_val = dest_ty.sentinel(zcu).?; + val.* = sentinel_val.toIntern(); + ref.* = Air.internedToRef(sentinel_val.toIntern()); + break; + } const index_ref = Air.internedToRef((try pt.intValue(.usize, i)).toIntern()); const src = inst_src; // TODO better source location const elem_src = inst_src; // TODO better source location diff --git a/test/behavior/array.zig b/test/behavior/array.zig index 574d0f2a007134d1bccc5bce2a300da0abd43fc7..97c595d474b3e06978e372dc47dca9c78d4eb31c 100644 --- a/test/behavior/array.zig +++ b/test/behavior/array.zig @@ -596,6 +596,21 @@ test "tuple to array handles sentinel" { try expect(S.b[3] == 0); } +test "array-like to array handles sentinel" { + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; + if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO + if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO + + const S = struct { + const a: @Vector(3, u8) = .{ 1, 2, 3 }; + var b: [3:0]u8 = a; + }; + try expect(S.b[0] == 1); + try expect(S.b[1] == 2); + try expect(S.b[2] == 3); + try expect(S.b[3] == 0); +} + test "array init of container level array variable" { if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO -- 2.54.0 From badf016b83583df98ac9c56e4147235fb9bd7f8c Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Mon, 19 Oct 2026 23:25:29 -0700 Subject: [PATCH 5/6] InternPool: include sentinel when checking for repeating elems Without including the sentinel in this check, a sentinel terminated array with repeated elements up to the sentinel would get interned as a repeated aggregate, meaning that when the value would get uninterned for mutation, all values including the sentinel would get set to the repeated element, even though the sentinel is *not* repeated. Without this fix, `x.a[2]` in the added test case would end up with the value 50. Closes #37339 --- src/InternPool.zig | 2 +- test/behavior/comptime_memory.zig | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/InternPool.zig b/src/InternPool.zig index 88c454fc2f3b414710886a524c80f4ecc7b09134..7df03f7dca76b86b765068a22f9e150d0f537e7b 100644 --- a/src/InternPool.zig +++ b/src/InternPool.zig @@ -7901,7 +7901,7 @@ pub fn get(ip: *InternPool, gpa: Allocator, io: Io, tid: Zcu.PerThread.Id, key: switch (aggregate.storage) { .bytes => |bytes| for (bytes.toSlice(len, ip)[1..]) |byte| if (byte != bytes.at(0, ip)) break :repeated, - .elems => |elems| for (elems[1..@intCast(len)]) |elem| + .elems => |elems| for (elems[1..@intCast(len_including_sentinel)]) |elem| if (elem != elems[0]) break :repeated, .repeated_elem => {}, } diff --git a/test/behavior/comptime_memory.zig b/test/behavior/comptime_memory.zig index 63044d026e18e7d77e9e049805867d8dc6749fc2..f72669473778d517cad3200d88fd304d25c4a92a 100644 --- a/test/behavior/comptime_memory.zig +++ b/test/behavior/comptime_memory.zig @@ -606,3 +606,15 @@ test "reinterpret pointer as optional pointer via double-pointer coercion" { comptime assert(@TypeOf(loaded) == ?*anyopaque); comptime assert(@intFromPtr(loaded) == 0x1000); } + +test "comptime store to sentinel-terminated array in an extern struct" { + comptime { + var x: extern struct { + a: [2:0]u8 = @splat(50), + } = .{}; + x.a[0] = 100; + assert(x.a[0] == 100); + assert(x.a[1] == 50); + assert(x.a[2] == 0); + } +} -- 2.54.0 From 711130742b747249e472c6d6b8d77d6692a28744 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Tue, 20 Oct 2026 00:22:26 -0700 Subject: [PATCH 6/6] InternPool: include sentinel when checking for repeating bytes --- src/InternPool.zig | 2 +- test/behavior/comptime_memory.zig | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/InternPool.zig b/src/InternPool.zig index 7df03f7dca76b86b765068a22f9e150d0f537e7b..aef6f6f036d13e50925662d7ddfbcab37d978de7 100644 --- a/src/InternPool.zig +++ b/src/InternPool.zig @@ -7899,7 +7899,7 @@ pub fn get(ip: *InternPool, gpa: Allocator, io: Io, tid: Zcu.PerThread.Id, key: repeated: { switch (aggregate.storage) { - .bytes => |bytes| for (bytes.toSlice(len, ip)[1..]) |byte| + .bytes => |bytes| for (bytes.toSlice(len_including_sentinel, ip)[1..]) |byte| if (byte != bytes.at(0, ip)) break :repeated, .elems => |elems| for (elems[1..@intCast(len_including_sentinel)]) |elem| if (elem != elems[0]) break :repeated, diff --git a/test/behavior/comptime_memory.zig b/test/behavior/comptime_memory.zig index f72669473778d517cad3200d88fd304d25c4a92a..e88fe23c86c294627dad536044be9528416edd1d 100644 --- a/test/behavior/comptime_memory.zig +++ b/test/behavior/comptime_memory.zig @@ -618,3 +618,13 @@ test "comptime store to sentinel-terminated array in an extern struct" { assert(x.a[2] == 0); } } + +test "comptime store to repeated string bytes" { + comptime { + var x = "aa".*; + x[0] = 'b'; + assert(x[0] == 'b'); + assert(x[1] == 'a'); + assert(x[2] == 0); + } +} -- 2.54.0