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
927927
928928 .deref => {
929929 const lhs = try expr(gz, scope, .{ .rl = .none }, tree.nodeData(node).node);
930 _ = try gz.addUnNode(.validate_deref, lhs, node);
931930 switch (ri.rl) {
932931 .ref,
933932 .ref_coerced_ty,
934933 .ref_const,
935 => return lhs,
934 => return gz.addUnNode(.ref_deref, lhs, node),
936935
937936 else => {
938 const result = try gz.addUnNode(.load, lhs, node);
937 const result = try gz.addUnNode(.deref, lhs, node);
939938 return rvalue(gz, ri, result, node);
940939 },
941940 }
......@@ -2759,6 +2758,8 @@ fn addEnsureResult(gz: *GenZir, maybe_unused_result: Zir.Inst.Ref, statement: As
27592758 .mulwrap,
27602759 .mul_sat,
27612760 .ref,
2761 .deref,
2762 .ref_deref,
27622763 .shl,
27632764 .shl_sat,
27642765 .shr,
......@@ -2932,7 +2933,6 @@ fn addEnsureResult(gz: *GenZir, maybe_unused_result: Zir.Inst.Ref, statement: As
29322933 .memcpy,
29332934 .memset,
29342935 .memmove,
2935 .validate_deref,
29362936 .validate_destructure,
29372937 .save_err_ret_index,
29382938 .restore_err_ret_index_unconditional,
lib/std/zig/Zir.zig+21-11
......@@ -567,12 +567,21 @@ pub const Inst = struct {
567567 /// Uses the `pl_node` field with payload `Bin`.
568568 merge_error_sets,
569569 /// 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 value
571 /// is `comptime`, the memory location is global static constant data. Otherwise,
572 /// the memory location is in the stack frame, local to the scope containing the
573 /// instruction.
570 /// stores it in a memory location, and returns a const single-item pointer to it.
571 /// If the value is `comptime`, the memory location is global static constant data.
572 /// Otherwise, the memory location is in the stack frame, local to the scope
573 /// containing the instruction.
574574 /// Uses the `un_tok` union field.
575575 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,
576585 /// Sends control flow back to the function's callee.
577586 /// Includes an operand as the return value.
578587 /// Includes an AST node source location.
......@@ -717,9 +726,6 @@ pub const Inst = struct {
717726 /// - `if (eu) |payload| {...} else |err| {...}`, AST node is the `if`.
718727 /// Uses the `pl_node` union field. Payload is `SwitchBlock`.
719728 switch_block_err_union,
720 /// Check that operand type supports the dereference operand (.*).
721 /// Uses the `un_node` field.
722 validate_deref,
723729 /// Check that the operand's type is an array or tuple with the given number of elements.
724730 /// Uses the `pl_node` field. Payload is `ValidateDestructure`.
725731 validate_destructure,
......@@ -1170,6 +1176,8 @@ pub const Inst = struct {
11701176 .mulwrap,
11711177 .mul_sat,
11721178 .ref,
1179 .deref,
1180 .ref_deref,
11731181 .shl,
11741182 .shl_sat,
11751183 .shr,
......@@ -1213,7 +1221,6 @@ pub const Inst = struct {
12131221 .switch_block,
12141222 .switch_block_ref,
12151223 .switch_block_err_union,
1216 .validate_deref,
12171224 .validate_destructure,
12181225 .union_init,
12191226 .field_type_ref,
......@@ -1352,7 +1359,6 @@ pub const Inst = struct {
13521359 .atomic_store,
13531360 .store_node,
13541361 .store_to_inferred_ptr,
1355 .validate_deref,
13561362 .validate_destructure,
13571363 .@"export",
13581364 .set_runtime_safety,
......@@ -1456,6 +1462,8 @@ pub const Inst = struct {
14561462 .mulwrap,
14571463 .mul_sat,
14581464 .ref,
1465 .deref,
1466 .ref_deref,
14591467 .shl,
14601468 .shl_sat,
14611469 .shr,
......@@ -1708,6 +1716,8 @@ pub const Inst = struct {
17081716 .merge_error_sets = .pl_node,
17091717 .mod_rem = .pl_node,
17101718 .ref = .un_tok,
1719 .deref = .un_node,
1720 .ref_deref = .un_node,
17111721 .ret_node = .un_node,
17121722 .ret_load = .un_node,
17131723 .ret_implicit = .un_tok,
......@@ -1744,7 +1754,6 @@ pub const Inst = struct {
17441754 .switch_block = .pl_node,
17451755 .switch_block_ref = .pl_node,
17461756 .switch_block_err_union = .pl_node,
1747 .validate_deref = .un_node,
17481757 .validate_destructure = .pl_node,
17491758 .field_type_ref = .pl_node,
17501759 .union_init = .pl_node,
......@@ -4186,6 +4195,8 @@ fn findTrackableInner(
41864195 .for_len,
41874196 .merge_error_sets,
41884197 .ref,
4198 .deref,
4199 .ref_deref,
41894200 .ret_node,
41904201 .ret_load,
41914202 .ret_implicit,
......@@ -4219,7 +4230,6 @@ fn findTrackableInner(
42194230 .enum_literal,
42204231 .decl_literal,
42214232 .decl_literal_no_coerce,
4222 .validate_deref,
42234233 .validate_destructure,
42244234 .field_type_ref,
42254235 .opt_eu_base_ptr_init,
src/Sema.zig+65-29
......@@ -1241,6 +1241,8 @@ fn analyzeBodyInner(
12411241 .optional_type => try sema.zirOptionalType(block, inst),
12421242 .ptr_type => try sema.zirPtrType(block, inst),
12431243 .ref => try sema.zirRef(block, inst),
1244 .deref => try sema.zirDeref(block, inst),
1245 .ref_deref => try sema.zirRefDeref(block, inst),
12441246 .shr => try sema.zirShr(block, inst, .shr),
12451247 .shr_exact => try sema.zirShr(block, inst, .shr_exact),
12461248 .slice_end => try sema.zirSliceEnd(block, inst),
......@@ -1566,11 +1568,6 @@ fn analyzeBodyInner(
15661568 i += 1;
15671569 continue;
15681570 },
1569 .validate_deref => {
1570 try sema.zirValidateDeref(block, inst);
1571 i += 1;
1572 continue;
1573 },
15741571 .validate_destructure => {
15751572 try sema.zirValidateDestructure(block, inst);
15761573 i += 1;
......@@ -3082,6 +3079,69 @@ fn zirRef(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Ins
30823079 return sema.analyzeRef(block, block.tokenOffset(inst_data.src_tok), operand, .none);
30833080}
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
30853145fn zirEnsureResultUsed(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {
30863146 const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;
30873147 const operand = sema.resolveInst(inst_data.operand);
......@@ -4606,30 +4666,6 @@ fn zirValidatePtrArrayInit(
46064666 }
46074667}
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
46334669fn zirValidateDestructure(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {
46344670 const pt = sema.pt;
46354671 const zcu = pt.zcu;
src/Zcu.zig+1-1
......@@ -1531,7 +1531,7 @@ pub const SrcLoc = struct {
15311531 .node_offset_deref_ptr => |node_off| {
15321532 const tree = try src_loc.file_scope.getTree(zcu);
15331533 const node = node_off.toAbsolute(src_loc.base_node);
1534 return tree.nodeToSpan(node);
1534 return tree.nodeToSpan(tree.nodeData(node).node);
15351535 },
15361536 .node_offset_asm_source => |node_off| {
15371537 const tree = try src_loc.file_scope.getTree(zcu);
src/print_zir.zig+2-1
......@@ -204,6 +204,8 @@ const Writer = struct {
204204 .ensure_result_used,
205205 .ensure_result_non_error,
206206 .ensure_err_union_payload_void,
207 .deref,
208 .ref_deref,
207209 .ret_node,
208210 .ret_load,
209211 .resolve_inferred_alloc,
......@@ -260,7 +262,6 @@ const Writer = struct {
260262 .bit_reverse,
261263 .@"resume",
262264 .make_ptr_const,
263 .validate_deref,
264265 .validate_const,
265266 .check_comptime_control_flow,
266267 .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" {
11361136
11371137 try expect(buf_b[0] == 1234);
11381138}
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" {
793793 const opt: ?*u8 = @ptrFromInt(0x1000);
794794 const outer_ptr: [*c]const ?*u8 = &opt;
795795 const inner_ptr = &outer_ptr.*.?;
796 comptime assert(@TypeOf(inner_ptr) == [*c]const *u8);
796 comptime assert(@TypeOf(inner_ptr) == *const *u8);
797797 comptime assert(@intFromPtr(inner_ptr.*) == 0x1000);
798798}
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'