Skip to content

Commit

Permalink
🚧 Drop toplevel properties dict in favour of julia Val-dispatch.
Browse files Browse the repository at this point in the history
  • Loading branch information
iago-lito committed Jul 29, 2024
1 parent bb88446 commit ef68433
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 58 deletions.
101 changes: 62 additions & 39 deletions src/Framework/methods.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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:
#
Expand All @@ -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

# ==========================================================================================
Expand Down
32 changes: 13 additions & 19 deletions src/Framework/system.jl
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# 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.
#
# The whole framework responsibility is to ensure consistency of the wrapped value.
# 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.

Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand Down

0 comments on commit ef68433

Please sign in to comment.