diff --git a/src/Framework/Framework.jl b/src/Framework/Framework.jl index 193868c8a..a7c3979ab 100644 --- a/src/Framework/Framework.jl +++ b/src/Framework/Framework.jl @@ -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") diff --git a/src/Framework/blueprint_macro.jl b/src/Framework/blueprint_macro.jl index 68fd96775..d7f041140 100644 --- a/src/Framework/blueprint_macro.jl +++ b/src/Framework/blueprint_macro.jl @@ -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}) = +# brought(::Blueprint) = generated iterator over the fields, skipping 'nothing' values. +# implied_blueprint_for(::Blueprint, ::Type{CompType}) = # # 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: # @@ -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, ) diff --git a/src/Framework/blueprints.jl b/src/Framework/blueprints.jl index 78e51f5ff..4212b7c22 100644 --- a/src/Framework/blueprints.jl +++ b/src/Framework/blueprints.jl @@ -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}()) diff --git a/src/Framework/component.jl b/src/Framework/component.jl index b54f8c812..737897dcf 100644 --- a/src/Framework/component.jl +++ b/src/Framework/component.jl @@ -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'.") diff --git a/src/Framework/method_macro.jl b/src/Framework/method_macro.jl index 46185476d..f9baa6ed8 100644 --- a/src/Framework/method_macro.jl +++ b/src/Framework/method_macro.jl @@ -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)) @@ -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( diff --git a/src/Framework/methods.jl b/src/Framework/methods.jl index 113a7328a..631dc70e6 100644 --- a/src/Framework/methods.jl +++ b/src/Framework/methods.jl @@ -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} = diff --git a/src/Framework/system.jl b/src/Framework/system.jl index 4ff17f81f..60ac0a0ed 100644 --- a/src/Framework/system.jl +++ b/src/Framework/system.jl @@ -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. @@ -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. @@ -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) @@ -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. @@ -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)") @@ -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)) #------------------------------------------------------------------------------------------- diff --git a/test/framework/01-regular_use.jl b/test/framework/01-regular_use.jl index 031c669b6..7ad5b3185 100644 --- a/test/framework/01-regular_use.jl +++ b/test/framework/01-regular_use.jl @@ -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 # ========================================================================================== @@ -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 @@ -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. @@ -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 diff --git a/test/framework/runtests.jl b/test/framework/runtests.jl index 858997c0c..d243ef338 100644 --- a/test/framework/runtests.jl +++ b/test/framework/runtests.jl @@ -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__)) @@ -26,4 +27,6 @@ else end end +Framework.LOCAL_MACROCALLS = false + end