Are "temporaries" always const?

I can take the address of an expression without assigning it to a variable first, but apparently it is const:

const std = @import("std");

fn f1() i32 {
    return 42;
}

fn f2(p: *const i32) void {
    std.debug.print("{}\n", .{p.*});
}

fn f3(p: *i32) void {
    p.* += 1;
    std.debug.print("{}\n", .{p.*});
}

pub fn main() void {
    f2(&f1()); // works
    f3(&f1()); // error: expected type '*i32', found '*const i32'
    f3(@constCast(&f1())); // this makes it work?
}

Is there a reason why it has to be immutable? As far as I understand it’s just a temporary value stored on the stack.

Is it safe to just constCast it?

Temporaries are always const and no its not safe to constCast it. Zig requires mutable references to always point to a location which clearly implies that it is backed by some sort of storage.

5 Likes

No.

fn forty_two() i32 {
    return 42;
}

fn plus_one(p: *const i32) i32 {
    @constCast(p).* += 1;
    return p.*;
}

fn sum_plus_two(p: *const i32, q: *const i32) i32 {
    return plus_one(p) + plus_one(q);
}

fn double_plus_two(p: *const i32) i32 {
    return sum_plus_two(p, p);
}


test "it is safe to @constCast" {
    try std.testing.expectEqual(86, double_plus_two(&forty_two())); // fail
}

It depends on how many times you reuse the pointer and how deep you hide the @constCast(). The compiler may think it is a good idea to reuse pointer to immutable value for optimization.

Your snippet is unrelated to constCast, but rather showcasing aliasing. constCast is unsafe because the address may not be in writable memory region.

Typically you only use constCast to bypass bad C apis. If you have a address that was obtained from a allocator, that is usually safe to constCast (though you still want to prefer fixing your types instead of relying on constCast)

2 Likes

All kinds of stupid can happen:

fn screw(str: []const u8) void {
    for (str, "fake") |*ch, f| {
        @constCast(ch).* = f;
    }
}

test "are my global constants safe?" {
    const data = "true".*;
    screw(&data);
    try std.testing.expectEqualStrings("true", &data);
    std.debug.print("true == {}\n", .{ true });
}

zig test:

true == fake
All 1 tests passed.

I mean I do not know what the compiler is allowed to alias if it thinks the values are immutable.

I have figured out how to trigger it:

inline fn f1() i32 {
    return 42;
}

fn f3(p: *i32) i32 {
    p.* += 1;
    std.debug.print("{}\n", .{p.*});
    return p.*;
}

test "it is safe to @constCast a temporary" {
    try std.testing.expectEqual(43, f3(@constCast(&f1())));
    try std.testing.expectEqual(43, f3(@constCast(&f1())));
}

zig test:

43
44
expected 43, found 44

zig version 0.16.0, default backend on x86_64 linux

edit:

$ zig-0.14.1 test lalala.zig
Segmentation fault at
...
p.* += 1;
1 Like

Yes, this makes sense. I guess what’s missing in Zig is something like a &(int){expression} compound literal in C which explicitly ensures that an object is stored in the stack frame.

That is by design, in the past temporaries were mutable. Just add an explicit variable.

2 Likes
const std = @import("std");
pub fn main() !void {
    const reader: *std.Io.Reader = .ending;
    // crashes on x86_64 linux with -fllvm
    _ = try reader.discardRemaining();
}

Is bit more real @constCast bug that you can encounter https://codeberg.org/ziglang/zig/issues/30070

1 Like