Better error message for modified structs in Julia >= 1.12

In Julia 1.12 it is now possible to edit structs (finally!!), with the objects instantiated using the “old” structs “assigned” to a world age table rather than the last “version” of the type.

The issue is when you try to use them according to the latest version of the type they belong:

mutable struct Point
    x::Float64
    y::Float64
    z::Int64 # Added later
end
a = Point(1.0, 2.0)    # Point(1.0, 2.0)
# added field z to Point..
b = Point(1.0, 2.0, 3) # Point(1.0, 2.0, 3)
a == b         # false
bt = typeof(b) # Point
at = typeof(a) # @world(Point,38522:38525)
at == bt       # false
b.z # 3
a.z # ERROR: FieldError: type Point has no field `z`, available fields: `x`, `y`

I can anticipate this will be a common issue for many :smile: :grin:

Wouldn’t then be better to look explicitly if the type of the object is in an “outdated” world?

  • option 1 (I guess less computationally expensive): just replace Point with @world(Point,38522:38525) in the error message
  • option 2: check if the type is in a different world age than the latest one and add to the message that the field/method may be available in a later world age
  • option 3: check if it is the case (field is available only on a later age) and advise user

Is there already an issue open for this ?

6 Likes

(1) agreed, it’s replaced in many other printouts so why not that one
(2, 3) Not helpful generally because you may not have a type assigned to const Point in the current world age, let alone a type with the property. Obsolete evaluations (instantiations, calls, annotations) are just a risk of interactivity, the same way we run into weird and sometimes silent behavior after caching results of invalidated methods.

1 Like

Indeed, it is a trade-off between a more case-specific error message that does some work or a more simple, general one.

I agree with just replace the type with the world age decorated version…

Here’s the full error message since it wasn’t shown in the OP:

julia> mutable struct Point
           x::Float64
           y::Float64
       end

julia> a = Point(1.0, 2.0);

julia> mutable struct Point
           x::Float64
           y::Float64
           z::Int64
       end

julia> a.z
ERROR: FieldError: type Point has no field `z`, available fields: `x`, `y`
Stacktrace:
 [1] getproperty(x::@world(Point, 38777:38780), f::Symbol)
   @ Base ./Base_compiler.jl:57
 [2] top-level scope
   @ REPL[4]:1

Notice that it does say in the stacktrace that this was caused by getproperty(x::@world(Point, 38777:38780), f::Symbol). Personally, I find this mostly clear, but I guess that’s because I’m used to reading stacktraces.

I’ve opened a PR that would implement Option 1, but I’m a bit conflicted on whether or not this actually makes things clearer or not. Open to thoughts / suggestions.

3 Likes

I prefer it to be upfront in the type’s printed name because the plain name Point seems to be consistently referring to the newest world age otherwise, and it doesn’t seem to be worth the risk of getting the wrong idea at reading the first few lines. Also, it’s possible for the call’s input types to be omitted by inlining or maybe some other reason. Note that the @inline in the following example was unnecessary, this problem shows up with automatic inlining too:

julia> mutable struct Point # v1.11.5
        x::Float64
        y::Float64
       end

julia> e() = @inline Point(1,2).z
e (generic function with 1 method)

julia> e()
ERROR: type Point has no field z
Stacktrace:
 [1] getproperty
   @ .\Base.jl:49 [inlined]
 [2] e()
   @ Main .\REPL[50]:1
 [3] top-level scope
   @ REPL[53]:1

julia> f() = @noinline Point(1,2).z
f (generic function with 1 method)

julia> f()
ERROR: type Point has no field z
Stacktrace:
 [1] getproperty(x::Point, f::Symbol)
   @ Base .\Base.jl:49
 [2] f()
   @ Main .\REPL[54]:1
 [3] top-level scope
   @ REPL[55]:1
3 Likes

That’s a great point. I’ve posted it in the PR as additional motivation for the change.