From 9ff926bb1e47ebad0f13835355841ef4654fbe60 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Fri, 12 Jun 2026 17:06:14 +0200 Subject: [PATCH] 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`. --- lib/std/zig/AstGen.zig | 8 +- lib/std/zig/Zir.zig | 32 ++++--- src/Sema.zig | 94 +++++++++++++------ src/Zcu.zig | 2 +- src/print_zir.zig | 3 +- test/behavior/array.zig | 25 +++++ test/behavior/pointers.zig | 2 +- .../deref_ptr_to_comptime_only_type.zig | 9 ++ .../ref_deref_of_null_c_ptr.zig | 8 ++ 9 files changed, 136 insertions(+), 47 deletions(-) create mode 100644 test/cases/compile_errors/deref_ptr_to_comptime_only_type.zig create mode 100644 test/cases/compile_errors/ref_deref_of_null_c_ptr.zig diff --git a/lib/std/zig/AstGen.zig b/lib/std/zig/AstGen.zig index b9084412f3b71875e3257efa08a6633839a37e2a..4adf7d0232804afe4738add9bf7a5307e2f1cdd3 100644 --- a/lib/std/zig/AstGen.zig +++ b/lib/std/zig/AstGen.zig @@ -927,15 +927,14 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE .deref => { const lhs = try expr(gz, scope, .{ .rl = .none }, tree.nodeData(node).node); - _ = try gz.addUnNode(.validate_deref, lhs, node); switch (ri.rl) { .ref, .ref_coerced_ty, .ref_const, - => return lhs, + => return gz.addUnNode(.ref_deref, lhs, node), else => { - const result = try gz.addUnNode(.load, lhs, node); + const result = try gz.addUnNode(.deref, lhs, node); return rvalue(gz, ri, result, node); }, } @@ -2759,6 +2758,8 @@ fn addEnsureResult(gz: *GenZir, maybe_unused_result: Zir.Inst.Ref, statement: As .mulwrap, .mul_sat, .ref, + .deref, + .ref_deref, .shl, .shl_sat, .shr, @@ -2932,7 +2933,6 @@ fn addEnsureResult(gz: *GenZir, maybe_unused_result: Zir.Inst.Ref, statement: As .memcpy, .memset, .memmove, - .validate_deref, .validate_destructure, .save_err_ret_index, .restore_err_ret_index_unconditional, diff --git a/lib/std/zig/Zir.zig b/lib/std/zig/Zir.zig index 62bfb64cfea37363775d4e9ce2e461ffe45d1aca..49897a755f8a08cedc86d421250a887afe03a720 100644 --- a/lib/std/zig/Zir.zig +++ b/lib/std/zig/Zir.zig @@ -567,12 +567,21 @@ pub const Inst = struct { /// Uses the `pl_node` field with payload `Bin`. merge_error_sets, /// Turns an R-Value into a const L-Value. In other words, it takes a value, - /// stores it in a memory location, and returns a const pointer to it. If the value - /// is `comptime`, the memory location is global static constant data. Otherwise, - /// the memory location is in the stack frame, local to the scope containing the - /// instruction. + /// stores it in a memory location, and returns a const single-item pointer to it. + /// If the value is `comptime`, the memory location is global static constant data. + /// Otherwise, the memory location is in the stack frame, local to the scope + /// containing the instruction. /// Uses the `un_tok` union field. ref, + /// Implements the dereference operand (`.*`). Checks that operand is a pointer + /// that supports being directly dereferenced. + /// Uses the `un_node` union field. + deref, + /// Emitted when a dereference (`.*`) with a reference result location occurs. + /// Checks that operand is a pointer that supports being directly dereferenced + /// and returns a single-item pointer to the dereferenced memory location. + /// Uses the `un_node` union field. + ref_deref, /// Sends control flow back to the function's callee. /// Includes an operand as the return value. /// Includes an AST node source location. @@ -717,9 +726,6 @@ pub const Inst = struct { /// - `if (eu) |payload| {...} else |err| {...}`, AST node is the `if`. /// Uses the `pl_node` union field. Payload is `SwitchBlock`. switch_block_err_union, - /// Check that operand type supports the dereference operand (.*). - /// Uses the `un_node` field. - validate_deref, /// Check that the operand's type is an array or tuple with the given number of elements. /// Uses the `pl_node` field. Payload is `ValidateDestructure`. validate_destructure, @@ -1170,6 +1176,8 @@ pub const Inst = struct { .mulwrap, .mul_sat, .ref, + .deref, + .ref_deref, .shl, .shl_sat, .shr, @@ -1213,7 +1221,6 @@ pub const Inst = struct { .switch_block, .switch_block_ref, .switch_block_err_union, - .validate_deref, .validate_destructure, .union_init, .field_type_ref, @@ -1352,7 +1359,6 @@ pub const Inst = struct { .atomic_store, .store_node, .store_to_inferred_ptr, - .validate_deref, .validate_destructure, .@"export", .set_runtime_safety, @@ -1456,6 +1462,8 @@ pub const Inst = struct { .mulwrap, .mul_sat, .ref, + .deref, + .ref_deref, .shl, .shl_sat, .shr, @@ -1708,6 +1716,8 @@ pub const Inst = struct { .merge_error_sets = .pl_node, .mod_rem = .pl_node, .ref = .un_tok, + .deref = .un_node, + .ref_deref = .un_node, .ret_node = .un_node, .ret_load = .un_node, .ret_implicit = .un_tok, @@ -1744,7 +1754,6 @@ pub const Inst = struct { .switch_block = .pl_node, .switch_block_ref = .pl_node, .switch_block_err_union = .pl_node, - .validate_deref = .un_node, .validate_destructure = .pl_node, .field_type_ref = .pl_node, .union_init = .pl_node, @@ -4186,6 +4195,8 @@ fn findTrackableInner( .for_len, .merge_error_sets, .ref, + .deref, + .ref_deref, .ret_node, .ret_load, .ret_implicit, @@ -4219,7 +4230,6 @@ fn findTrackableInner( .enum_literal, .decl_literal, .decl_literal_no_coerce, - .validate_deref, .validate_destructure, .field_type_ref, .opt_eu_base_ptr_init, diff --git a/src/Sema.zig b/src/Sema.zig index 4ee9bbb01f9ad58b38947cd0fa8295b62064e82b..5ee2d914d032cd120cbc65bc9369afbc139cda23 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -1241,6 +1241,8 @@ fn analyzeBodyInner( .optional_type => try sema.zirOptionalType(block, inst), .ptr_type => try sema.zirPtrType(block, inst), .ref => try sema.zirRef(block, inst), + .deref => try sema.zirDeref(block, inst), + .ref_deref => try sema.zirRefDeref(block, inst), .shr => try sema.zirShr(block, inst, .shr), .shr_exact => try sema.zirShr(block, inst, .shr_exact), .slice_end => try sema.zirSliceEnd(block, inst), @@ -1566,11 +1568,6 @@ fn analyzeBodyInner( i += 1; continue; }, - .validate_deref => { - try sema.zirValidateDeref(block, inst); - i += 1; - continue; - }, .validate_destructure => { try sema.zirValidateDestructure(block, inst); i += 1; @@ -3082,6 +3079,69 @@ fn zirRef(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Ins return sema.analyzeRef(block, block.tokenOffset(inst_data.src_tok), operand, .none); } +fn zirDeref(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref { + const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node; + const src = block.nodeOffset(inst_data.src_node); + const ptr_src = block.src(.{ .node_offset_deref_ptr = inst_data.src_node }); + const operand = sema.resolveInst(inst_data.operand); + + try sema.validateDeref(block, src, operand, sema.typeOf(operand)); + + return sema.analyzeLoad(block, src, operand, ptr_src); +} + +fn zirRefDeref(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref { + const pt = sema.pt; + const zcu = pt.zcu; + const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node; + const src = block.nodeOffset(inst_data.src_node); + const ptr_src = block.src(.{ .node_offset_deref_ptr = inst_data.src_node }); + const operand = sema.resolveInst(inst_data.operand); + const operand_ty = sema.typeOf(operand); + + try sema.validateDeref(block, src, operand, operand_ty); + + const ptr_info = operand_ty.ptrInfo(zcu); + return switch (ptr_info.flags.size) { + .many, .slice => unreachable, // cannot be dereferenced + .c => ptr: { + var single_ptr_flags = ptr_info.flags; + single_ptr_flags.size = .one; + single_ptr_flags.is_allowzero = false; + const single_ptr_ty = try pt.ptrType(.{ + .child = ptr_info.child, + .flags = single_ptr_flags, + }); + break :ptr try sema.coerceCompatiblePtrs(block, single_ptr_ty, operand, ptr_src); + }, + .one => operand, + }; +} + +fn validateDeref( + sema: *Sema, + block: *Block, + src: LazySrcLoc, + ref: Air.Inst.Ref, + ty: Type, +) CompileError!void { + const pt = sema.pt; + const zcu = pt.zcu; + if (ty.zigTypeTag(zcu) != .pointer) { + return sema.fail(block, src, "cannot dereference non-pointer type '{f}'", .{ty.fmt(pt)}); + } else switch (ty.ptrSize(zcu)) { + .one, .c => {}, + .many => return sema.fail(block, src, "index syntax required for unknown-length pointer type '{f}'", .{ty.fmt(pt)}), + .slice => return sema.fail(block, src, "index syntax required for slice type '{f}'", .{ty.fmt(pt)}), + } + if (sema.resolveValue(ref)) |val| { + // Error for deref of undef pointer, unless the pointee is OPV in which case it's legal. + if (val.isUndef(zcu) and ty.childType(zcu).classify(zcu) != .one_possible_value) { + return sema.fail(block, src, "cannot dereference undefined value", .{}); + } + } +} + fn zirEnsureResultUsed(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void { const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node; const operand = sema.resolveInst(inst_data.operand); @@ -4606,30 +4666,6 @@ fn zirValidatePtrArrayInit( } } -fn zirValidateDeref(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void { - const pt = sema.pt; - const zcu = pt.zcu; - const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node; - const src = block.nodeOffset(inst_data.src_node); - const operand = sema.resolveInst(inst_data.operand); - const operand_ty = sema.typeOf(operand); - - if (operand_ty.zigTypeTag(zcu) != .pointer) { - return sema.fail(block, src, "cannot dereference non-pointer type '{f}'", .{operand_ty.fmt(pt)}); - } else switch (operand_ty.ptrSize(zcu)) { - .one, .c => {}, - .many => return sema.fail(block, src, "index syntax required for unknown-length pointer type '{f}'", .{operand_ty.fmt(pt)}), - .slice => return sema.fail(block, src, "index syntax required for slice type '{f}'", .{operand_ty.fmt(pt)}), - } - - if (sema.resolveValue(operand)) |val| { - // Error for deref of undef pointer, unless the pointee is OPV in which case it's legal. - if (val.isUndef(zcu) and operand_ty.childType(zcu).classify(zcu) != .one_possible_value) { - return sema.fail(block, src, "cannot dereference undefined value", .{}); - } - } -} - fn zirValidateDestructure(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void { const pt = sema.pt; const zcu = pt.zcu; diff --git a/src/Zcu.zig b/src/Zcu.zig index 487e102009674671ec141e9cec9c06e9af6eb91f..ee3063051f28947bad9c9248189b953aefb96204 100644 --- a/src/Zcu.zig +++ b/src/Zcu.zig @@ -1531,7 +1531,7 @@ pub const SrcLoc = struct { .node_offset_deref_ptr => |node_off| { const tree = try src_loc.file_scope.getTree(zcu); const node = node_off.toAbsolute(src_loc.base_node); - return tree.nodeToSpan(node); + return tree.nodeToSpan(tree.nodeData(node).node); }, .node_offset_asm_source => |node_off| { const tree = try src_loc.file_scope.getTree(zcu); diff --git a/src/print_zir.zig b/src/print_zir.zig index 300cc25a74bcca965e0f8088ec123a0d45f98481..64703fbd60052209854fc733570fa008ec5b201e 100644 --- a/src/print_zir.zig +++ b/src/print_zir.zig @@ -204,6 +204,8 @@ const Writer = struct { .ensure_result_used, .ensure_result_non_error, .ensure_err_union_payload_void, + .deref, + .ref_deref, .ret_node, .ret_load, .resolve_inferred_alloc, @@ -260,7 +262,6 @@ const Writer = struct { .bit_reverse, .@"resume", .make_ptr_const, - .validate_deref, .validate_const, .check_comptime_control_flow, .opt_eu_base_ptr_init, diff --git a/test/behavior/array.zig b/test/behavior/array.zig index 8f6a0898b362259f1e7733cc6359d33d5451d4ab..1ac512b628353f37b53a31816e5cc7913e42836c 100644 --- a/test/behavior/array.zig +++ b/test/behavior/array.zig @@ -1136,3 +1136,28 @@ test "resist alias of explicit copy of array passed as arg" { try expect(buf_b[0] == 1234); } + +test "access element through reference" { + const S = struct { + fn doTheTest(x: u8) !void { + { + var val: [1]u8 = .{x}; + const single_ptr: *[1]u8 = &val; + try expect(single_ptr.*[0] == x); + const elem_ptr = &single_ptr.*[0]; + comptime assert(@TypeOf(elem_ptr) == *u8); + try expect(elem_ptr.* == x); + } + { + var val: [1]u8 = .{x}; + const c_ptr: [*c][1]u8 = &val; + try expect(c_ptr.*[0] == x); + const elem_ptr = &c_ptr.*[0]; + comptime assert(@TypeOf(elem_ptr) == *u8); + try expect(elem_ptr.* == x); + } + } + }; + try comptime S.doTheTest(123); + try S.doTheTest(123); +} diff --git a/test/behavior/pointers.zig b/test/behavior/pointers.zig index 97aafa6ea7bcc3a0cd54dbfb08fb9f72c0a274ac..4bd48a4ca91ed4f6ccc313ab960cfdde9887b0e3 100644 --- a/test/behavior/pointers.zig +++ b/test/behavior/pointers.zig @@ -793,6 +793,6 @@ test "comptime C pointer to optional pointer" { const opt: ?*u8 = @ptrFromInt(0x1000); const outer_ptr: [*c]const ?*u8 = &opt; const inner_ptr = &outer_ptr.*.?; - comptime assert(@TypeOf(inner_ptr) == [*c]const *u8); + comptime assert(@TypeOf(inner_ptr) == *const *u8); comptime assert(@intFromPtr(inner_ptr.*) == 0x1000); } diff --git a/test/cases/compile_errors/deref_ptr_to_comptime_only_type.zig b/test/cases/compile_errors/deref_ptr_to_comptime_only_type.zig new file mode 100644 index 0000000000000000000000000000000000000000..cc453d616fbe4dd836a069a034e61498f1b33dce --- /dev/null +++ b/test/cases/compile_errors/deref_ptr_to_comptime_only_type.zig @@ -0,0 +1,9 @@ +export fn entry(ptr: *anyopaque) void { + const ct_only: *type = @ptrCast(ptr); + _ = ct_only.*; +} + +// error +// +// :3:16: error: cannot load comptime-only type 'type' +// :3:9: note: pointer of type '*type' is runtime-known diff --git a/test/cases/compile_errors/ref_deref_of_null_c_ptr.zig b/test/cases/compile_errors/ref_deref_of_null_c_ptr.zig new file mode 100644 index 0000000000000000000000000000000000000000..85d186b4a06d131e079e33f4a2ebbb7f0d669016 --- /dev/null +++ b/test/cases/compile_errors/ref_deref_of_null_c_ptr.zig @@ -0,0 +1,8 @@ +export fn entry() void { + const ptr: [*c]u8 = null; + _ = &ptr.*; +} + +// error +// +// :3:10: error: null pointer casted to type '*u8' -- 2.54.0