Skip to content

Commit

Permalink
🚧 Improve properties() to list depencencies.
Browse files Browse the repository at this point in the history
  • Loading branch information
iago-lito committed Aug 2, 2024
1 parent 56d7af4 commit 4b55ceb
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 71 deletions.
25 changes: 23 additions & 2 deletions src/Framework/Framework.jl
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,39 @@ using Crayons
using MacroTools
using OrderedCollections

# Convenience aliases.
argerr(m) = throw(ArgumentError(m))
const Option{T} = Union{T,Nothing}
struct PhantomData{T} end
const imap = Iterators.map
const ifilter = Iterators.filter

# ==========================================================================================
# Early small types declarations to avoid circular dependencies among code files.

# Abstract over various exception thrown during inconsistent use of the system.
abstract type SystemException <: Exception end

# The parametric type 'V' for the component
# is the type of the value wrapped by the system.
abstract type Component{V} end
abstract type Blueprint{V} end

export Component
export Blueprint

# Most framework internals work with component types
# because they can be abstract,
# most exposed methods work with concrete singleton instance.
const CompType{V} = Type{<:Component{V}}

# ==========================================================================================

# Base structure.
include("./component.jl")
include("./system.jl") # <- Defines the 'System' type used in the next files..
include("./component.jl") # <- But better start reading from this file.
include("./blueprints.jl")
include("./methods.jl")
include("./system.jl")
include("./add.jl")
include("./plus_operator.jl")

Expand Down
12 changes: 6 additions & 6 deletions src/Framework/blueprint_macro.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@
# is automatically considered 'potential brought': the macro invocation makes it work.
# The following methods are relevant then:
#
# brought(::Name) = generated iterator over the fields, skipping 'nothing' values.
# implied_blueprint_for(::Name, ::Type{CompType}) = <assumed implemented by macro invoker>
# brought(::Blueprint) = generated iterator over the fields, skipping 'nothing' values.
# implied_blueprint_for(::Blueprint, ::Type{CompType}) = <assumed implemented by macro invoker>
#
# And for blueprint user convenience, override:
#
# setproperty!(::Name, field, value)
# setproperty!(::Blueprint, field, value)
#
# When given `nothing` as a value, void the field.
# When given a blueprint, check its provided components for consistency then embed.
# When given a comptype or a singleton component instance, make it implied.
# When given anything else, query the following for a callable blueprint constructor:
#
# constructor_for_embedded(::Name, ::Val{fieldname}) = Component
# # (default, not reified/overrideable yet)
# constructor_for_embedded(::Blueprint, ::Val{fieldname}) = Component
# # (defaults to the provided component if single, not reified/overrideable yet)
#
# then pass whatever value to this constructor to get this sugar:
#
Expand Down Expand Up @@ -241,7 +241,7 @@ function blueprint_macro(__module__, __source__, input...)
)
bp
end
setfield!(b, prop, val)
setfield!(b, prop, BroughtField{expected_C, ValueType}(val))
end
end,
)
Expand Down
3 changes: 0 additions & 3 deletions src/Framework/blueprints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@
# This blueprint requirement is specified by the 'expands_from' function.
# Expanding-from an abstract component A is expanding from any component subtyping A.

abstract type Blueprint{V} end
export Blueprint

# Every blueprint provides only concrete components, and at least one.
struct UnspecifiedComponents{B<:Blueprint} end
componentsof(::B) where {B<:Blueprint} = throw(UnspecifiedComponents{B}())
Expand Down
12 changes: 2 additions & 10 deletions src/Framework/component.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,8 @@
# This might be implemented in the future for a convincingly motivated need,
# at the cost of extending @component macro to produce abstract types.

# The parametric type 'V' for the component
# is the type of the value wrapped by the system.
abstract type Component{V} end
export Component

# Most framework internals work with component types
# because they can be abstract,
# most exposed methods work with concrete singleton instance.
const CompType{V} = Type{<:Component{V}}
Base.convert(::CompType{V}, c::Component{V}) where {V} = typeof(c) # Singleton ergonomy.
# Singleton component ergonomy.
Base.convert(::CompType{V}, c::Component{V}) where {V} = typeof(c)

# Component types being singleton, we *can* infer the value from the type at runtime.
singleton_instance(C::CompType) = throw("No concrete singleton instance of '$C'.")
Expand Down
19 changes: 1 addition & 18 deletions src/Framework/method_macro.jl
Original file line number Diff line number Diff line change
Expand Up @@ -359,23 +359,6 @@ function method_macro(__module__, __source__, input...)
Framework.depends(::Type{ValueType}, ::Type{typeof(fn)}) = deps
end)

# Generate the method checking that required components
# are loaded on the system instance.
push_res!(
quote
function Framework.missing_dependency_for(
::Type{ValueType},
M::Type{typeof(fn)},
s::System,
)
for dep in depends(ValueType, M)
has_component(s, dep) || return dep
end
nothing
end
end,
)

# Override the detected methods with checked code receiving system values.
efn = LOCAL_MACROCALLS ? esc(fn_xp) : Meta.quot(:($__module__.$fn_xp))
fn_path = Meta.quot(Meta.quot(fn_xp))
Expand All @@ -388,7 +371,7 @@ function method_macro(__module__, __source__, input...)
xp = quote
function $($efn)(; kwargs...)
# function $mod.$fnname(; kwargs...)
$dep = missing_dependency_for($ValueType, $($efn), $receiver)
$dep = missing_dependency_for($($efn), $receiver)
if !isnothing($dep)
$a = isabstracttype($dep) ? " a" : ""
throw(
Expand Down
19 changes: 15 additions & 4 deletions src/Framework/methods.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,23 @@
# The wrapped system value type must always be specified.

# Methods depend on nothing by default.
depends(V::Type, ::Type{<:Function}) = CompType{V}[]
missing_dependency_for(::Type, ::Type{<:Function}, _) = nothing
depends(::Type{V}, ::Type{<:Function}) where {V} = CompType{V}[]
missing_dependencies_for(fn::Type{<:Function}, s::System{V}) where {V} =
Iterators.filter(depends(V, fn)) do dep
!has_component(s, dep)
end
# Just pick the first one. Return nothing if dependencies are met.
function missing_dependency_for(fn::Type{<:Function}, s::System)
for dep in missing_dependencies_for(fn, s)
return dep
end
nothing
end

# Direct call with the functions themselves.
depends(V::Type, fn::Function) = depends(V, typeof(fn))
missing_dependency_for(V::Type, fn::Function, s) = missing_dependency_for(V, typeof(fn), s)
depends(::Type{V}, fn::Function) where {V} = depends(V, typeof(fn))
missing_dependencies_for(fn::Function, s::System) = missing_dependencies_for(typeof(fn), s)
missing_dependency_for(fn::Function, s::System) = missing_dependency_for(typeof(fn), s)

# Map wrapped system value and property name to the corresponding function.
read_property(V::Type, ::Val{name}) where {name} =
Expand Down
63 changes: 38 additions & 25 deletions src/Framework/system.jl
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ function Base.getproperty(system::System{V}, p::Symbol) where {V}
# Search property method.
fn = read_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"
miss = missing_dependency_for(fn, system)
if !isnothing(miss)
comp = isabstracttype(miss) ? "A component $miss" : "Component $miss"
properr(V, p, "$comp is required to read this property.")
end
# Forward to method.
Expand All @@ -88,9 +88,9 @@ function Base.setproperty!(system::System{V}, p::Symbol, rhs) where {V}
# Search property method.
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"
miss = missing_dependency_for(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.
Expand Down Expand Up @@ -119,8 +119,8 @@ end
# Query components.

# Iterate over all concrete components.
component_types(s::System) = Iterators.map(identity, s._concrete)
components(s::System) = Iterators.map(singleton_instance, component_types(s))
component_types(s::System) = imap(identity, s._concrete)
components(s::System) = imap(singleton_instance, component_types(s))
# Restrict to the given component (super)type.
components_types(system::System{V}, C::CompType{V}) where {V} =
if isabstracttype(C)
Expand All @@ -131,7 +131,7 @@ components_types(system::System{V}, C::CompType{V}) where {V} =
C in d ? (C,) : ()
end
components(s::System, C::CompType{V}) where {V} =
Iterators.map(singleton_instance, components_types(s, C))
imap(singleton_instance, components_types(s, C))
export components, component_types

# Basic check.
Expand All @@ -141,10 +141,11 @@ has_concrete_component(s::System{V}, c::Component{V}) where {V} = typeof(c) in s
export has_component, has_concrete_component

# List all properties and associated functions for this type.
# Yields (name, fn_read, Option{fn_write}) values.
# Yields (property_name, fn_read, Option{fn_write}, iterator{dependencies...}).
function properties(::Type{V}) where {V}
Iterators.map(
Iterators.filter(methods(read_property, Tuple{Type{V},Val}, Framework)) do mth
imap(
ifilter(methods(read_property, Tuple{Type{V},Val}, Framework)) do mth
mth.sig isa UnionAll && return false # Only consider concrete implementations.
val = mth.sig.types[end]
val <: Val ||
throw("Unexpected method signature for $read_property: $(mth.sig)")
Expand All @@ -153,27 +154,39 @@ function properties(::Type{V}) where {V}
) do mth
val = mth.sig.types[end]
name = first(val.parameters)
(name, read_property(V, Val(name)), possible_write_property(V, Val(name)))
read_fn = read_property(V, Val(name))
write_fn = possible_write_property(V, Val(name))
(name, read_fn, write_fn, imap(identity, depends(V, read_fn)))
end
end
# Also feature for system type directly.
properties(::Type{System{V}}) where {V} = properties(V)
export properties

# List properties available for this system type.
# Returns {:propname => is_writeable}
function properties(::Type{System{V}}) where {V}
Dict(p => isnothing(write) for (p, read, write) in properties(V))
# List properties available for *this* particular instance.
# Yields (:propname, read, Option{write})
function properties(s::System{V}) where {V}
imap(ifilter(properties(V)) do (_, read, _, _)
isnothing(missing_dependency_for(read, s))
end) do (name, read, write, _)
(name, read, write)
end
end

# List properties available for this instance.
function properties(s::System{V}) where {V}
Dict(
p => isnothing(write) for
(p, read, write) in properties(V) if isnothing(missing_dependency_for(V, read, s))
)
# List *unavailable* properties for this instance
# along with the components missing to support them.
# Yields (:propname, read, Option{write}, iterator{missing_dependencies...})
function latent_properties(s::System{V}) where {V}
imap(ifilter(properties(V)) do (_, read, _, _)
!isnothing(missing_dependency_for(read, s))
end) do (name, read, write, deps)
(name, read, write, ifilter(d -> !has_component(s, d), deps))
end
end
export properties
export latent_properties

# Consistency + REPL completion.
Base.propertynames(s::System{V}) where {V} = collect(keys(properties(s)))
Base.propertynames(s::System) = imap(first, properties(s))

#-------------------------------------------------------------------------------------------

Expand Down
33 changes: 30 additions & 3 deletions test/framework/01-regular_use.jl
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,33 @@ end
sr = sa + ReflectFromB()
@test sr.reflection == collect("12321")

#---------------------------------------------------------------------------------------
# List properties.

# All possible system properties and their dependencies.
props = properties(typeof(sa))
@test sort(map(((n, r, w, g),) -> (n, r, w, F.singleton_instance.(g)), props)) == [
(:a, get_a, nothing, Component[A]),
(:b, get_b, nothing, Component[B]),
(:n, get_n, nothing, Component[Size]),
(:ref, get_reflection, set_reflection!, Component[Reflection]),
(:reflection, get_reflection, set_reflection!, Component[Reflection]),
(:sum, get_sum, nothing, Component[A, B]),
]

# Only the ones available on this instance.
props = properties(sa)
@test sort(collect(props)) == [(:a, get_a, nothing), (:n, get_n, nothing)]

# Only the ones *missing* on this instance.
props = latent_properties(sa)
@test sort(map(((n, r, w, g),) -> (n, r, w, F.singleton_instance.(g)), props)) == [
(:b, get_b, nothing, Component[B]),
(:ref, get_reflection, set_reflection!, Component[Reflection]),
(:reflection, get_reflection, set_reflection!, Component[Reflection]),
(:sum, get_sum, nothing, Component[B]),
]

end

# ==========================================================================================
Expand Down Expand Up @@ -323,7 +350,7 @@ end
# Embedded blueprints.

# Explicitly bring it instead of implying.
a.size = NLines(2) # HERE: check Base.setproperty! definition in blueprint_macro.jl.
a.size = NLines(2)

# The component is also brought.
s = e + a
Expand All @@ -346,7 +373,7 @@ end

# And it is an error to bring it if the component is already there.
s = e + NLines(2) # (*even* if the data are consistent)
@sysfails(s += a, Add(BroughtAlreadyInValue, [NLines, false, A.Raw]))
@sysfails(s += a, Add(BroughtAlreadyInValue, Size, [NLines, false, A.Raw]))

#---------------------------------------------------------------------------------------
# Unbrought blueprints.
Expand All @@ -355,7 +382,7 @@ end
a.size = nothing

# The component is not brought then.
@sysfails(e + a, Add(MissingRequiredComponent, Size, [A.Raw], nothing, false))
@sysfails(e + a, Add(MissingRequiredComponent, A, Size, [A.Raw], nothing))

end

Expand Down
3 changes: 3 additions & 0 deletions test/framework/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Framework.LOCAL_MACROCALLS = true
# Run all numbered -.jl files we can find by default, except the current one.
only = [
"./01-regular_use.jl",
# "./02-blueprints.jl",
] # Unless some files are specified here, in which case only run these.
if isempty(only)
for (folder, _, files) in walkdir(dirname(@__FILE__))
Expand All @@ -26,4 +27,6 @@ else
end
end

Framework.LOCAL_MACROCALLS = false

end

0 comments on commit 4b55ceb

Please sign in to comment.