Some are properly marked readonly in declarations, some are not[1]
But this is a common problem with unmarked logging. The compiler will infer attributes all the way through things it has bitcode for. (IE it can determine it to be readonly, readnone, whatever, as long as it can see calls to either functions marked readonly/readnone, or determine the called functions to be readonly/readnone)
But, usually, if you don't give it the whole program, it can't infer that your logging function does not modify its arguments.
[1] Fun fact: glibc allows printf handler registration: https://www.gnu.org/software/libc/manual/html_node/Customizi...
So sadly, it's printf is not readonly, because it can do anything it wants in these callbacks.
(nobody uses the printf registration, though, so it may be time to just add a compiler option required to make it work, and not screw everyone else using printf over)
So you wouldn't see this there.
The call to the print is
call void @_ZN3std2io5stdio6_print17h690779b3bd8114d5E(%"core::fmt::Arguments"* noalias nocapture nonnull dereferenceable(48) %_3)
The entire chunk of `main` preceding that call: %size = alloca i64, align 8
%_3 = alloca %"core::fmt::Arguments", align 8
%_8 = alloca [1 x %"core::fmt::ArgumentV1"], align 8
%0 = bitcast i64* %size to i8*
call void @llvm.lifetime.start(i64 8, i8* %0)
store i64 33554432, i64* %size, align 8
%1 = bitcast %"core::fmt::Arguments"* %_3 to i8*
call void @llvm.lifetime.start(i64 48, i8* %1)
%2 = bitcast [1 x %"core::fmt::ArgumentV1"]* %_8 to i8*
call void @llvm.lifetime.start(i64 16, i8* %2)
%3 = ptrtoint i64* %size to i64
%4 = bitcast [1 x %"core::fmt::ArgumentV1"]* %_8 to i64*
store i64 %3, i64* %4, align 8
%5 = getelementptr inbounds [1 x %"core::fmt::ArgumentV1"], [1 x %"core::fmt::ArgumentV1"]* %_8, i64 0, i64 0, i32 1
%6 = bitcast i8 (%"core::fmt::Void"*, %"core::fmt::Formatter"*)** %5 to i64*
store i64 ptrtoint (i8 (i64*, %"core::fmt::Formatter"*)* @"_ZN4core3fmt3num54_$LT$impl$u20$core..fmt..Display$u20$for$u20$usize$GT$3fmt17hb872170870cc06d9E" to i64), i64* %6, align 8
%7 = getelementptr inbounds [1 x %"core::fmt::ArgumentV1"], [1 x %"core::fmt::ArgumentV1"]* %_8, i64 0, i64 0
%8 = getelementptr inbounds %"core::fmt::Arguments", %"core::fmt::Arguments"* %_3, i64 0, i32 0, i32 0
store %str_slice* getelementptr inbounds ([2 x %str_slice], [2 x %str_slice]* @ref.8, i64 0, i64 0), %str_slice** %8, align 8, !alias.scope !1, !noalias !4
%9 = getelementptr inbounds %"core::fmt::Arguments", %"core::fmt::Arguments"* %_3, i64 0, i32 0, i32 1
store i64 2, i64* %9, align 8, !alias.scope !1, !noalias !4
%_6.sroa.0.0..sroa_idx.i = getelementptr inbounds %"core::fmt::Arguments", %"core::fmt::Arguments"* %_3, i64 0, i32 1, i32 0, i32 0
store %"core::fmt::rt::v1::Argument"* null, %"core::fmt::rt::v1::Argument"** %_6.sroa.0.0..sroa_idx.i, align 8, !alias.scope !1, !noalias !4
%10 = getelementptr inbounds %"core::fmt::Arguments", %"core::fmt::Arguments"* %_3, i64 0, i32 2, i32 0
store %"core::fmt::ArgumentV1"* %7, %"core::fmt::ArgumentV1"** %10, align 8, !alias.scope !1, !noalias !4
%11 = getelementptr inbounds %"core::fmt::Arguments", %"core::fmt::Arguments"* %_3, i64 0, i32 2, i32 1
store i64 1, i64* %11, align 8, !alias.scope !1, !noalias !4
call void @_ZN3std2io5stdio6_print17h690779b3bd8114d5E(%"core::fmt::Arguments"* noalias nocapture nonnull dereferenceable(48) %_3)
[1]: https://play.rust-lang.org/It's not the same issue but it's the same area of stupid stuff you have to find out about and roll your eyes.