authorgravatar for justus@klausecker.deJustus Klausecker <justus@klausecker.de> 2026-06-12 17:06:14+02:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-06-16 12:58:34+02:00
log9ff926bb1e47ebad0f13835355841ef4654fbe60
tree976c2e9cdac3f0944a68ac6343f144700f1e299a
parentf3607c75cc072df8ad52d3ae8ff6bcfb2dfeeb8f

frontend: guarantee references to dereference expressions to be single-item pointers

AstGen doesn't emit a `ref` ZIR inst if it encounters a dereference `.*` with a reference result location (`ref`/`ref_coerced_ty`/`ref_const`), since the operand of the dereference expression already is a reference. This caused some problems as Zig allows `.*` on both single-item *and* C pointers, but most logic in Sema assumes that everything with a reference rl is always a single-item pointer. This largely didn't cause any issues since C pointers can do everything single-item pointers can, but it caused Sema to mistake `&p.*[0]` with `@TypeOf(p) == [*c][n]T` for an index into a many-item pointer `[*][n]T` instead of a single-item pointer `*[n]T`. To avoid such problems in the future, a pointer obtained from `&p.*` is now always turned into a proper single-item pointer. This is achieved by introducing two new ZIR instructions, `deref` and `ref_deref`: For `ptr.*`: ``` %1 = validate_deref(%ptr) %2 = load(%ptr) ``` vvv ``` %1 = deref(%ptr) ``` For `&ptr.*`: ``` %1 = validate_deref(%ptr) // use %ptr directly ``` vvv ``` %1 = ref_deref(%ptr) // use %1 ``` This makes `validate_deref` superfluous, so it's been removed. Also fixes `node_offset_deref_ptr` source location to actually point to the pointer being dereferenced and uses it to provide better source locs for `deref` and `ref_deref`.

9 files changed, 136 insertions(+), 47 deletions(-)

lib/std/zig/AstGen.zig+4-4
...@@ -927,15 +927,14 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE...@@ -927,15 +927,14 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE
927927
928 .deref => {928 .deref => {
929 const lhs = try expr(gz, scope, .{ .rl = .none }, tree.nodeData(node).node);929 const lhs = try expr(gz, scope, .{ .rl = .none }, tree.nodeData(node).node);
930 _ = try gz.addUnNode(.validate_deref, lhs, node);
931 switch (ri.rl) {930 switch (ri.rl) {
932 .ref,931 .ref,
933 .ref_coerced_ty,932 .ref_coerced_ty,
934 .ref_const,933 .ref_const,
935 => return lhs,934 => return gz.addUnNode(.ref_deref, lhs, node),
936935
937 else => {936 else => {
938 const result = try gz.addUnNode(.load, lhs, node);937 const result = try gz.addUnNode(.deref, lhs, node);
939 return rvalue(gz, ri, result, node);938 return rvalue(gz, ri, result, node);
940 },939 },
941 }940 }
...@@ -2759,6 +2758,8 @@ fn addEnsureResult(gz: *GenZir, maybe_unused_result: Zir.Inst.Ref, statement: As...@@ -2759,6 +2758,8 @@ fn addEnsureResult(gz: *GenZir, maybe_unused_result: Zir.Inst.Ref, statement: As
2759 .mulwrap,2758 .mulwrap,
2760 .mul_sat,2759 .mul_sat,
2761 .ref,2760 .ref,
2761 .deref,
2762 .ref_deref,
2762 .shl,2763 .shl,
2763 .shl_sat,2764 .shl_sat,
2764 .shr,2765 .shr,
...@@ -2932,7 +2933,6 @@ fn addEnsureResult(gz: *GenZir, maybe_unused_result: Zir.Inst.Ref, statement: As...@@ -2932,7 +2933,6 @@ fn addEnsureResult(gz: *GenZir, maybe_unused_result: Zir.Inst.Ref, statement: As
2932 .memcpy,2933 .memcpy,
2933 .memset,2934 .memset,
2934 .memmove,2935 .memmove,
2935 .validate_deref,
2936 .validate_destructure,2936 .validate_destructure,
2937 .save_err_ret_index,2937 .save_err_ret_index,
2938 .restore_err_ret_index_unconditional,2938 .restore_err_ret_index_unconditional,
lib/std/zig/Zir.zig+21-11
...@@ -567,12 +567,21 @@ pub const Inst = struct {...@@ -567,12 +567,21 @@ pub const Inst = struct {
567 /// Uses the `pl_node` field with payload `Bin`.567 /// Uses the `pl_node` field with payload `Bin`.
568 merge_error_sets,568 merge_error_sets,
569 /// Turns an R-Value into a const L-Value. In other words, it takes a value,569 /// Turns an R-Value into a const L-Value. In other words, it takes a value,
570 /// stores it in a memory location, and returns a const pointer to it. If the value570 /// stores it in a memory location, and returns a const single-item pointer to it.
571 /// is `comptime`, the memory location is global static constant data. Otherwise,571 /// If the value is `comptime`, the memory location is global static constant data.
572 /// the memory location is in the stack frame, local to the scope containing the572 /// Otherwise, the memory location is in the stack frame, local to the scope
573 /// instruction.573 /// containing the instruction.
574 /// Uses the `un_tok` union field.574 /// Uses the `un_tok` union field.
575 ref,575 ref,
576 /// Implements the dereference operand (`.*`). Checks that operand is a pointer
577 /// that supports being directly dereferenced.
578 /// Uses the `un_node` union field.
579 deref,
580 /// Emitted when a dereference (`.*`) with a reference result location occurs.
581 /// Checks that operand is a pointer that supports being directly dereferenced
582 /// and returns a single-item pointer to the dereferenced memory location.
583 /// Uses the `un_node` union field.
584 ref_deref,
576 /// Sends control flow back to the function's callee.585 /// Sends control flow back to the function's callee.
577 /// Includes an operand as the return value.586 /// Includes an operand as the return value.
578 /// Includes an AST node source location.587 /// Includes an AST node source location.
...@@ -717,9 +726,6 @@ pub const Inst = struct {...@@ -717,9 +726,6 @@ pub const Inst = struct {
717 /// - `if (eu) |payload| {...} else |err| {...}`, AST node is the `if`.726 /// - `if (eu) |payload| {...} else |err| {...}`, AST node is the `if`.
718 /// Uses the `pl_node` union field. Payload is `SwitchBlock`.727 /// Uses the `pl_node` union field. Payload is `SwitchBlock`.
719 switch_block_err_union,728 switch_block_err_union,
720 /// Check that operand type supports the dereference operand (.*).
721 /// Uses the `un_node` field.
722 validate_deref,
723 /// Check that the operand's type is an array or tuple with the given number of elements.729 /// Check that the operand's type is an array or tuple with the given number of elements.
724 /// Uses the `pl_node` field. Payload is `ValidateDestructure`.730 /// Uses the `pl_node` field. Payload is `ValidateDestructure`.
725 validate_destructure,731 validate_destructure,
...@@ -1170,6 +1176,8 @@ pub const Inst = struct {...@@ -1170,6 +1176,8 @@ pub const Inst = struct {
1170 .mulwrap,1176 .mulwrap,
1171 .mul_sat,1177 .mul_sat,
1172 .ref,1178 .ref,
1179 .deref,
1180 .ref_deref,
1173 .shl,1181 .shl,
1174 .shl_sat,1182 .shl_sat,
1175 .shr,1183 .shr,
...@@ -1213,7 +1221,6 @@ pub const Inst = struct {...@@ -1213,7 +1221,6 @@ pub const Inst = struct {
1213 .switch_block,1221 .switch_block,
1214 .switch_block_ref,1222 .switch_block_ref,
1215 .switch_block_err_union,1223 .switch_block_err_union,
1216 .validate_deref,
1217 .validate_destructure,1224 .validate_destructure,
1218 .union_init,1225 .union_init,
1219 .field_type_ref,1226 .field_type_ref,
...@@ -1352,7 +1359,6 @@ pub const Inst = struct {...@@ -1352,7 +1359,6 @@ pub const Inst = struct {
1352 .atomic_store,1359 .atomic_store,
1353 .store_node,1360 .store_node,
1354 .store_to_inferred_ptr,1361 .store_to_inferred_ptr,
1355 .validate_deref,
1356 .validate_destructure,1362 .validate_destructure,
1357 .@"export",1363 .@"export",
1358 .set_runtime_safety,1364 .set_runtime_safety,
...@@ -1456,6 +1462,8 @@ pub const Inst = struct {...@@ -1456,6 +1462,8 @@ pub const Inst = struct {
1456 .mulwrap,1462 .mulwrap,
1457 .mul_sat,1463 .mul_sat,
1458 .ref,1464 .ref,
1465 .deref,
1466 .ref_deref,
1459 .shl,1467 .shl,
1460 .shl_sat,1468 .shl_sat,
1461 .shr,1469 .shr,
...@@ -1708,6 +1716,8 @@ pub const Inst = struct {...@@ -1708,6 +1716,8 @@ pub const Inst = struct {
1708 .merge_error_sets = .pl_node,1716 .merge_error_sets = .pl_node,
1709 .mod_rem = .pl_node,1717 .mod_rem = .pl_node,
1710 .ref = .un_tok,1718 .ref = .un_tok,
1719 .deref = .un_node,
1720 .ref_deref = .un_node,
1711 .ret_node = .un_node,1721 .ret_node = .un_node,
1712 .ret_load = .un_node,1722 .ret_load = .un_node,
1713 .ret_implicit = .un_tok,1723 .ret_implicit = .un_tok,
...@@ -1744,7 +1754,6 @@ pub const Inst = struct {...@@ -1744,7 +1754,6 @@ pub const Inst = struct {
1744 .switch_block = .pl_node,1754 .switch_block = .pl_node,
1745 .switch_block_ref = .pl_node,1755 .switch_block_ref = .pl_node,
1746 .switch_block_err_union = .pl_node,1756 .switch_block_err_union = .pl_node,
1747 .validate_deref = .un_node,
1748 .validate_destructure = .pl_node,1757 .validate_destructure = .pl_node,
1749 .field_type_ref = .pl_node,1758 .field_type_ref = .pl_node,
1750 .union_init = .pl_node,1759 .union_init = .pl_node,
...@@ -4186,6 +4195,8 @@ fn findTrackableInner(...@@ -4186,6 +4195,8 @@ fn findTrackableInner(
4186 .for_len,4195 .for_len,
4187 .merge_error_sets,4196 .merge_error_sets,
4188 .ref,4197 .ref,
4198 .deref,
4199 .ref_deref,
4189 .ret_node,4200 .ret_node,
4190 .ret_load,4201 .ret_load,
4191 .ret_implicit,4202 .ret_implicit,
...@@ -4219,7 +4230,6 @@ fn findTrackableInner(...@@ -4219,7 +4230,6 @@ fn findTrackableInner(
4219 .enum_literal,4230 .enum_literal,
4220 .decl_literal,4231 .decl_literal,
4221 .decl_literal_no_coerce,4232 .decl_literal_no_coerce,
4222 .validate_deref,
4223 .validate_destructure,4233 .validate_destructure,
4224 .field_type_ref,4234 .field_type_ref,
4225 .opt_eu_base_ptr_init,4235 .opt_eu_base_ptr_init,
src/Sema.zig+65-29
...@@ -1241,6 +1241,8 @@ fn analyzeBodyInner(...@@ -1241,6 +1241,8 @@ fn analyzeBodyInner(
1241 .optional_type => try sema.zirOptionalType(block, inst),1241 .optional_type => try sema.zirOptionalType(block, inst),
1242 .ptr_type => try sema.zirPtrType(block, inst),1242 .ptr_type => try sema.zirPtrType(block, inst),
1243 .ref => try sema.zirRef(block, inst),1243 .ref => try sema.zirRef(block, inst),
1244 .deref => try sema.zirDeref(block, inst),
1245 .ref_deref => try sema.zirRefDeref(block, inst),
1244 .shr => try sema.zirShr(block, inst, .shr),1246 .shr => try sema.zirShr(block, inst, .shr),
1245 .shr_exact => try sema.zirShr(block, inst, .shr_exact),1247 .shr_exact => try sema.zirShr(block, inst, .shr_exact),
1246 .slice_end => try sema.zirSliceEnd(block, inst),1248 .slice_end => try sema.zirSliceEnd(block, inst),
...@@ -1566,11 +1568,6 @@ fn analyzeBodyInner(...@@ -1566,11 +1568,6 @@ fn analyzeBodyInner(
1566 i += 1;1568 i += 1;
1567 continue;1569 continue;
1568 },1570 },
1569 .validate_deref => {
1570 try sema.zirValidateDeref(block, inst);
1571 i += 1;
1572 continue;
1573 },
1574 .validate_destructure => {1571 .validate_destructure => {
1575 try sema.zirValidateDestructure(block, inst);1572 try sema.zirValidateDestructure(block, inst);
1576 i += 1;1573 i += 1;
...@@ -3082,6 +3079,69 @@ fn zirRef(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Ins...@@ -3082,6 +3079,69 @@ fn zirRef(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Ins
3082 return sema.analyzeRef(block, block.tokenOffset(inst_data.src_tok), operand, .none);3079 return sema.analyzeRef(block, block.tokenOffset(inst_data.src_tok), operand, .none);
3083}3080}
30843081
3082fn zirDeref(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
3083 const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;
3084 const src = block.nodeOffset(inst_data.src_node);
3085 const ptr_src = block.src(.{ .node_offset_deref_ptr = inst_data.src_node });
3086 const operand = sema.resolveInst(inst_data.operand);
3087
3088 try sema.validateDeref(block, src, operand, sema.typeOf(operand));
3089
3090 return sema.analyzeLoad(block, src, operand, ptr_src);
3091}
3092
3093fn zirRefDeref(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
3094 const pt = sema.pt;
3095 const zcu = pt.zcu;
3096 const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;
3097 const src = block.nodeOffset(inst_data.src_node);
3098 const ptr_src = block.src(.{ .node_offset_deref_ptr = inst_data.src_node });
3099 const operand = sema.resolveInst(inst_data.operand);
3100 const operand_ty = sema.typeOf(operand);
3101
3102 try sema.validateDeref(block, src, operand, operand_ty);
3103
3104 const ptr_info = operand_ty.ptrInfo(zcu);
3105 return switch (ptr_info.flags.size) {
3106 .many, .slice => unreachable, // cannot be dereferenced
3107 .c => ptr: {
3108 var single_ptr_flags = ptr_info.flags;
3109 single_ptr_flags.size = .one;
3110 single_ptr_flags.is_allowzero = false;
3111 const single_ptr_ty = try pt.ptrType(.{
3112 .child = ptr_info.child,
3113 .flags = single_ptr_flags,
3114 });
3115 break :ptr try sema.coerceCompatiblePtrs(block, single_ptr_ty, operand, ptr_src);
3116 },
3117 .one => operand,
3118 };
3119}
3120
3121fn validateDeref(
3122 sema: *Sema,
3123 block: *Block,
3124 src: LazySrcLoc,
3125 ref: Air.Inst.Ref,
3126 ty: Type,
3127) CompileError!void {
3128 const pt = sema.pt;
3129 const zcu = pt.zcu;
3130 if (ty.zigTypeTag(zcu) != .pointer) {
3131 return sema.fail(block, src, "cannot dereference non-pointer type '{f}'", .{ty.fmt(pt)});
3132 } else switch (ty.ptrSize(zcu)) {
3133 .one, .c => {},
3134 .many => return sema.fail(block, src, "index syntax required for unknown-length pointer type '{f}'", .{ty.fmt(pt)}),
3135 .slice => return sema.fail(block, src, "index syntax required for slice type '{f}'", .{ty.fmt(pt)}),
3136 }
3137 if (sema.resolveValue(ref)) |val| {
3138 // Error for deref of undef pointer, unless the pointee is OPV in which case it's legal.
3139 if (val.isUndef(zcu) and ty.childType(zcu).classify(zcu) != .one_possible_value) {
3140 return sema.fail(block, src, "cannot dereference undefined value", .{});
3141 }
3142 }
3143}
3144
3085fn zirEnsureResultUsed(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {3145fn zirEnsureResultUsed(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {
3086 const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;3146 const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;
3087 const operand = sema.resolveInst(inst_data.operand);3147 const operand = sema.resolveInst(inst_data.operand);
...@@ -4606,30 +4666,6 @@ fn zirValidatePtrArrayInit(...@@ -4606,30 +4666,6 @@ fn zirValidatePtrArrayInit(
4606 }4666 }
4607}4667}
46084668
4609fn zirValidateDeref(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {
4610 const pt = sema.pt;
4611 const zcu = pt.zcu;
4612 const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;
4613 const src = block.nodeOffset(inst_data.src_node);
4614 const operand = sema.resolveInst(inst_data.operand);
4615 const operand_ty = sema.typeOf(operand);
4616
4617 if (operand_ty.zigTypeTag(zcu) != .pointer) {
4618 return sema.fail(block, src, "cannot dereference non-pointer type '{f}'", .{operand_ty.fmt(pt)});
4619 } else switch (operand_ty.ptrSize(zcu)) {
4620 .one, .c => {},
4621 .many => return sema.fail(block, src, "index syntax required for unknown-length pointer type '{f}'", .{operand_ty.fmt(pt)}),
4622 .slice => return sema.fail(block, src, "index syntax required for slice type '{f}'", .{operand_ty.fmt(pt)}),
4623 }
4624
4625 if (sema.resolveValue(operand)) |val| {
4626 // Error for deref of undef pointer, unless the pointee is OPV in which case it's legal.
4627 if (val.isUndef(zcu) and operand_ty.childType(zcu).classify(zcu) != .one_possible_value) {
4628 return sema.fail(block, src, "cannot dereference undefined value", .{});
4629 }
4630 }
4631}
4632
4633fn zirValidateDestructure(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {4669fn zirValidateDestructure(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {
4634 const pt = sema.pt;4670 const pt = sema.pt;
4635 const zcu = pt.zcu;4671 const zcu = pt.zcu;
src/Zcu.zig+1-1
...@@ -1531,7 +1531,7 @@ pub const SrcLoc = struct {...@@ -1531,7 +1531,7 @@ pub const SrcLoc = struct {
1531 .node_offset_deref_ptr => |node_off| {1531 .node_offset_deref_ptr => |node_off| {
1532 const tree = try src_loc.file_scope.getTree(zcu);1532 const tree = try src_loc.file_scope.getTree(zcu);
1533 const node = node_off.toAbsolute(src_loc.base_node);1533 const node = node_off.toAbsolute(src_loc.base_node);
1534 return tree.nodeToSpan(node);1534 return tree.nodeToSpan(tree.nodeData(node).node);
1535 },1535 },
1536 .node_offset_asm_source => |node_off| {1536 .node_offset_asm_source => |node_off| {
1537 const tree = try src_loc.file_scope.getTree(zcu);1537 const tree = try src_loc.file_scope.getTree(zcu);
src/print_zir.zig+2-1
...@@ -204,6 +204,8 @@ const Writer = struct {...@@ -204,6 +204,8 @@ const Writer = struct {
204 .ensure_result_used,204 .ensure_result_used,
205 .ensure_result_non_error,205 .ensure_result_non_error,
206 .ensure_err_union_payload_void,206 .ensure_err_union_payload_void,
207 .deref,
208 .ref_deref,
207 .ret_node,209 .ret_node,
208 .ret_load,210 .ret_load,
209 .resolve_inferred_alloc,211 .resolve_inferred_alloc,
...@@ -260,7 +262,6 @@ const Writer = struct {...@@ -260,7 +262,6 @@ const Writer = struct {
260 .bit_reverse,262 .bit_reverse,
261 .@"resume",263 .@"resume",
262 .make_ptr_const,264 .make_ptr_const,
263 .validate_deref,
264 .validate_const,265 .validate_const,
265 .check_comptime_control_flow,266 .check_comptime_control_flow,
266 .opt_eu_base_ptr_init,267 .opt_eu_base_ptr_init,
test/behavior/array.zig+25
...@@ -1136,3 +1136,28 @@ test "resist alias of explicit copy of array passed as arg" {...@@ -1136,3 +1136,28 @@ test "resist alias of explicit copy of array passed as arg" {
11361136
1137 try expect(buf_b[0] == 1234);1137 try expect(buf_b[0] == 1234);
1138}1138}
1139
1140test "access element through reference" {
1141 const S = struct {
1142 fn doTheTest(x: u8) !void {
1143 {
1144 var val: [1]u8 = .{x};
1145 const single_ptr: *[1]u8 = &val;
1146 try expect(single_ptr.*[0] == x);
1147 const elem_ptr = &single_ptr.*[0];
1148 comptime assert(@TypeOf(elem_ptr) == *u8);
1149 try expect(elem_ptr.* == x);
1150 }
1151 {
1152 var val: [1]u8 = .{x};
1153 const c_ptr: [*c][1]u8 = &val;
1154 try expect(c_ptr.*[0] == x);
1155 const elem_ptr = &c_ptr.*[0];
1156 comptime assert(@TypeOf(elem_ptr) == *u8);
1157 try expect(elem_ptr.* == x);
1158 }
1159 }
1160 };
1161 try comptime S.doTheTest(123);
1162 try S.doTheTest(123);
1163}
test/behavior/pointers.zig+1-1
...@@ -793,6 +793,6 @@ test "comptime C pointer to optional pointer" {...@@ -793,6 +793,6 @@ test "comptime C pointer to optional pointer" {
793 const opt: ?*u8 = @ptrFromInt(0x1000);793 const opt: ?*u8 = @ptrFromInt(0x1000);
794 const outer_ptr: [*c]const ?*u8 = &opt;794 const outer_ptr: [*c]const ?*u8 = &opt;
795 const inner_ptr = &outer_ptr.*.?;795 const inner_ptr = &outer_ptr.*.?;
796 comptime assert(@TypeOf(inner_ptr) == [*c]const *u8);796 comptime assert(@TypeOf(inner_ptr) == *const *u8);
797 comptime assert(@intFromPtr(inner_ptr.*) == 0x1000);797 comptime assert(@intFromPtr(inner_ptr.*) == 0x1000);
798}798}
test/cases/compile_errors/deref_ptr_to_comptime_only_type.zig created+9
...@@ -0,0 +1,9 @@
1export fn entry(ptr: *anyopaque) void {
2 const ct_only: *type = @ptrCast(ptr);
3 _ = ct_only.*;
4}
5
6// error
7//
8// :3:16: error: cannot load comptime-only type 'type'
9// :3:9: note: pointer of type '*type' is runtime-known
test/cases/compile_errors/ref_deref_of_null_c_ptr.zig created+8
...@@ -0,0 +1,8 @@
1export fn entry() void {
2 const ptr: [*c]u8 = null;
3 _ = &ptr.*;
4}
5
6// error
7//
8// :3:10: error: null pointer casted to type '*u8'