diff --git a/src/Framework/methods.jl b/src/Framework/methods.jl index 78fec657c..ad2f7c744 100644 --- a/src/Framework/methods.jl +++ b/src/Framework/methods.jl @@ -10,16 +10,21 @@ # # Only the second one needs to be specified by the framework user, # along with its components dependencies. - +# +# If the method needs to run differently +# depending on the presence/absence of other components, +# then the following hook is provided: +# +# - method(v::Value, ..; .., _system::System) <- to enable has_component(_system, C). +# # Also, methods with these exact signatures: # -# - method(v::Value) +# - method(v::Value) (+ optional `; _system`) # -# - method!(v::Value, rhs) +# - method!(v::Value, rhs) ( + optional `; _system`) # # Can optionally become properties of the system/value, # in the sense of julia's `getproperty/set_property`. -# This requires module-level bookkeeping of the associated property names. # # The properties also come in two styles: # @@ -38,52 +43,70 @@ missing_dependency_for(::Type, ::Type{<:Function}, _) = nothing depends(V::Type, fn::Function) = depends(V, typeof(fn)) missing_dependency_for(V::Type, fn::Function, s) = missing_dependency_for(V, typeof(fn), s) -mutable struct Property - read::Function - write::Union{Nothing,Function} # Leave blank if the property is read-only. - Property(read) = new(read, nothing) -end +# Map wrapped system value and property name to the corresponding function. +read_property(V::Type, ::Val{name}) where {name} = + throw(PropertyError(V, name, "Unknown property.")) +write_property(V::Type, ::Val{name}) where {name} = + throw(PropertyError(V, name, "Unknown property.")) -# {SystemWrappedValueType => {:propname => property_functions}} -const PropDict = Dict{Symbol,Property} -properties_(::Type) = PropDict() -# When specialized, the above method yields a reference to underlying value, -# updated according to this module's own logic. Don't expose. +has_read_property(V::Type, name::Symbol) = + try + read_property(V, Val(name)) + true + catch e + e isa PropertyError || rethrow(e) + false + end + +has_write_property(V::Type, name::Symbol) = + try + write_property(V, Val(name)) + true + catch e + e isa PropertyError || rethrow(e) + false + end + +function readwrite_property(V::Type, name::Symbol) + read_property(V, Val(name)) # Errors if not even 'read-'. + try + write_property(V, Val(name)) + catch e + e isa PropertyError || rethrow(e) + properr(V, name, "This property is read-only.") + end +end # Hack flag to avoid that the checks below interrupt the `Revise` process. # Raise when done defining properties in the package. global REVISING = false # Set read property first.. -function set_read_property!(V::Type, name::Symbol, mth::Function) - current = properties_(V) - (haskey(current, name) && !REVISING) && +function set_read_property!(V::Type, name::Symbol, fn::Function) + REVISING || + has_read_property(V, name) || properr(V, name, "Readable property already exists.") - if isempty(current) - # Dynamically add method to lend reference to the value lended by `properties_`. - eval(quote - properties_(::Type{$V}) = $current - end) - end - # Or just mutating this value is enough. - current[name] = Property(mth) + # Dynamically add method to connect property name to the given function. + name = Meta.quot(name) + eval(quote + read_property(::Type{$V}, ::Val{$name}) = $fn + end) end # .. and only after, and optionally, the corresponding write property. -function set_write_property!(V::Type, name::Symbol, mth::Function) - current = properties_(V) - if !haskey(current, name) - properr( - V, - name, - "Property cannot be set writable \ - without having been set readable first.", - ) - end - prop = current[name] - (isnothing(prop.write) || REVISING) || - properr(V, name, "Writable property already exists.") - prop.write = mth +function set_write_property!(V::Type, name::Symbol, fn::Function) + has_read_property(V, name) || properr( + V, + name, + "Property cannot be set writable \ + without having been set readable first.", + ) + REVISING || + has_write_property(V, name) && properr(V, name, "Writable property already exists.") + name = Meta.quot(name) + eval(quote + write_property(::Type{$V}, ::Val{$name}) = $fn + end) end # ========================================================================================== diff --git a/src/Framework/system.jl b/src/Framework/system.jl index 3d45f63aa..d6a2c2ddf 100644 --- a/src/Framework/system.jl +++ b/src/Framework/system.jl @@ -1,5 +1,5 @@ # Components append data to a value wrapped in a 'System'. -# The system keeps track of all the blueprints used to add components their dependencies. +# The system keeps track of all the components added. # It also checks that the method called and properties invoked # do meet the components requirements. # @@ -7,6 +7,9 @@ # As a consequence, don't leak references to it or its inner data # unless user cannot corrupt the value state through them. # +# The value wrapped needs to be constructible from no arguments, +# so the user can start with an "empty system". +# # The value wrapped needs to be copy-able for the system to be forked. # So do the components blueprints. @@ -68,9 +71,7 @@ function Base.getproperty(system::System{V}, p::Symbol) where {V} # Authorize direct accesses to private fields. p in fieldnames(System) && return getfield(system, p) # Search property method. - props = properties_(V) - haskey(props, p) || syserr(V, "Invalid property name: '$p'.") - fn = props[p].read + fn = read_property(V, Val(p)) # Check for required components availability. Miss = missing_dependency_for(V, fn, system) if !isnothing(Miss) @@ -85,39 +86,32 @@ function Base.setproperty!(system::System{V}, p::Symbol, rhs) where {V} # Authorize direct accesses to private fields. p in fieldnames(System) && return setfield!(system, p, rhs) # Search property method. - props = properties_(V) - haskey(props, p) || syserr(V, "Invalid property name: '$p'.") - fn = props[p].write - isnothing(fn) && properr(V, p, "This property is read-only.") + fn = readwrite_property(V, Val(p)) # Check for required components availability. Miss = missing_dependency_for(V, fn, system) if !isnothing(Miss) comp = isabstracttype(Miss) ? "A component $Miss" : "Component $Miss" properr(V, p, "$comp is required to write to this property.") end - # Invoke property method, checking for available components. + # Invoke property method. fn(system, rhs) end -# In case the client agrees, +# In case the framework user agrees, # also forward the properties to the wrapped value. -# Note that this happens without checking dependent components. +# Note that this happens without checking dependent components, +# and that the `; _system` hook cannot be provided then in this context. # Still, a lot of things *are* checked, so this 'unchecked' does *not* mean 'performant'. function unchecked_getproperty(value::V, p::Symbol) where {V} - perr(mess) = properr(V, p, mess) p in fieldnames(V) && return getfield(value, p) - props = properties_(V) - haskey(props, p) || perr("Neither a field of '$V' nor a property.") - props[p].read(value) + fn = read_property(V, Val(p)) + fn(value) end function unchecked_setproperty!(value::V, p::Symbol, rhs) where {V} perr(mess) = properr(V, p, mess) p in fieldnames(V) && return setfield!(value, p, rhs) - props = properties_(V) - haskey(props, p) || perr("Neither a field or a property.") - fn = props[p].write - isnothing(fn) && properr(V, p, "Property is not writable.") + fn = readwrite_property(V, p) fn(value, rhs) end