But keep in mind the underlying data is still mutable, it’s just that BigFloat isn’t a mutable struct any more. In particular, MutableArithmetics.jl, an important low-level package, still works fine for BigFloat, on nightly too:
julia> using MutableArithmetics: MutableArithmetics as MA
julia> x = big(.1)
0.1000000000000000055511151231257827021181583404541015625
julia> MA.operate!(+, x, x)
0.200000000000000011102230246251565404236316680908203125
julia> x
0.200000000000000011102230246251565404236316680908203125
Thanks! I guess the good news is that BorrowChecker.is_static already flags these for moves (since the Ptr is mutable), so no changes are needed.
Though it’s a bit frightening that doing copy on an array of BigFloat will result in an array that is aliased to the original
julia> x = big.(randn(32));
julia> y = copy(x); # shallow copy not enough
julia> MA.operate!(+, y[1], 3);
I guess this would modify x[1] too, which is kinda terrifying. At what point do the perf gains outweigh this much debugging difficulty… For this reason I feel like there probably shouldn’t ever be a public interface?
I’m not sure if this is actually documented, but the official story for Number (thus BigInt and BigFloat) is “treat it as immutable”. That justifies the behavior of copy(::BigFloat) being the identity.
On the other hand, the MutableArithmetics.jl interfaces are perfectly sane, too, in particular, mutable_copy and copy_if_mutable are provided:
It is enforced throughout the public API, even going so far as to special case === and objectid to compare by value instead of memory address. String is clearly meant to behave as an immutable type in every respect. I suppose you can grab a pointer and go wild, but it’s probably not a relevant design goal for BorrowChecker.jl to accommodate such experiments.
NOTE: For technical reasons, ismutable returns true for values of certain special types (for example String and Symbol) even though they cannot be mutated in a permissible way.
The main reason String is treated as mutable is that any operation that creates a string requires mutating a String using some internal and slightly hacky functionality.
Thanks. Just curious: why would a low-level string precursor necessitate marking the resultant String object as mutable in user-space? Or does it actually create a String and then mutate it from within Julia?
Or does it actually create a String and then mutate it from within Julia?
yes.
string(a::Union{Char, String, SubString{String}, Symbol}...) = _string(a...)
function _string(a::Union{Char, String, SubString{String}, Symbol}...)
n = 0
for v in a
# 4 types is too many for automatic Union-splitting, so we split manually
# and allow one specializable call site per concrete type
if v isa Char
n += ncodeunits(v)
elseif v isa String
n += sizeof(v)
elseif v isa SubString{String}
n += sizeof(v)
else
n += sizeof(v::Symbol)
end
end
out = _string_n(n)
offs = 1
for v in a
if v isa Char
offs += __unsafe_string!(out, v, offs)
elseif v isa String || v isa SubString{String}
offs += __unsafe_string!(out, v, offs)
else
offs += __unsafe_string!(out, v::Symbol, offs)
end
end
return out
end
Thanks. I wonder if making a special internal _MutableString type for mutation at that stage would be better? That way String could actually be marked as immutable so that the compiler could do constant folding in user code (??)