Skip to content

Commit

Permalink
🚧 Setup triggers on particular combinations of components: FRAMEWORK …
Browse files Browse the repository at this point in the history
…READY :D

This settles the large framework refactoring for #139.
Now, the whole components library needs to be upgraded.
  • Loading branch information
iago-lito committed Sep 20, 2024
1 parent 1853363 commit 07640fe
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 21 deletions.
4 changes: 3 additions & 1 deletion src/Framework/Framework.jl
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ module Framework
# - [x] Recurring pattern: various blueprints types provide 'the same component': reify.
# - [x] `depends(other_method_name)` to inherit all dependent components.
# - [x] Namespace properties into like system.namespace.namespace.property.
# - [ ] Hooks need to trigger when special components combination become available.
# - [.] Hooks need to trigger when special components combination become available.
# See for instance the expansion of `Nutrients.Nodes`
# which should trigger the creation of links if there is already `Species`.. or vice
# versa.
Expand Down Expand Up @@ -125,6 +125,8 @@ export Blueprint
# because they can be abstract,
# most exposed methods work with concrete singleton instance.
const CompType{V} = Type{<:Component{V}}
# Abstract over either for exposed inputs.
const CompRef{V} = Union{Component{V},CompType{V}}

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

Expand Down
80 changes: 68 additions & 12 deletions src/Framework/add.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# - Visit post-order again to:
# - Run the late `check`.
# - Expand the blueprint into a component.
# - Execute possible triggers.
#
# TODO: Exposing the first analysis steps of the above will be useful
# to implement default_model.
Expand All @@ -34,7 +35,7 @@
# by the blueprints given.
# Reify the underlying 'forest' structure.
struct Node
blueprint::Blueprint # Owned copy, so it cannot be changed by add! caller afterwards.
blueprint::Blueprint # Owned copy, so it doesn't leave refs to add! caller.
parent::Option{Node}
implied::Bool # Raise if 'implied' by the parent and not 'embedded'.
children::Vector{Node}
Expand Down Expand Up @@ -235,6 +236,29 @@ function add!(system::System{V}, blueprints::Blueprint{V}...) where {V}

try

# Construct a copy all possible triggers,
# pruned from components already in-place.
# Triggers execute whenever a newly added component
# makes one of them empty.
# TODO: should this sophisticated 'decreasing counter'
# rather belong to the system itself?
# Pros: alleviate calculations during `add!`
# Cons: clutters `System` fields instead:
# every system would starts 'full' with all potential future triggers.
triggers = OrderedDict()
current_components = Set()
for C in component_types(system)
push!(current_components, C)
for sup in supertypes(C)
push!(current_components, sup)
end
end
for (combination, fns) in triggers_(V)
consumed = setdiff(combination, current_components)
isempty(consumed) && continue
triggers[combination] = (consumed, fns)
end

# Second post-order visit: expand the blueprints.
for Chk in keys(checked)
node = first(brought[Chk])
Expand All @@ -257,25 +281,44 @@ function add!(system::System{V}, blueprints::Blueprint{V}...) where {V}
end
end

# Expand.
try
expand!(system._value, blueprint, system)
catch _
throw(ExpansionAborted(node))
end

# Record.
just_added = Set()
for C in componentsof(blueprint)
crt, abs = system._concrete, system._abstract
push!(crt, C)
push!(just_added, C)
for sup in supertypes(C)
sup === C && continue
sup === Component{V} && break
sub = haskey(abs, sup) ? abs[sup] : (abs[sup] = Set{CompType{V}}())
push!(sub, C)
push!(just_added, C)
end
end

# Expand.
try
expand!(system._value, blueprint, system)
catch _
throw(ExpansionAborted(node))
# Execute possible triggers.
for (combination, (remaining, trigs)) in triggers
setdiff!(remaining, just_added)
if isempty(remaining)
for trig in trigs
try
trig(system._value, system)
catch _
throw(TriggerAborted(node, combination))
end
end
pop!(triggers, combination)
end
end


end

catch e
Expand All @@ -291,23 +334,31 @@ function add!(system::System{V}, blueprints::Blueprint{V}...) where {V}
else
# This is unexpected and it may have occured during expansion.
# The underlying system state consistency is no longuer guaranteed.
if e isa ExpansionAborted
raise = if e isa ExpansionAborted
title = "Failure during blueprint expansion."
subtitle = "This is a bug in the components library."
epilog = render_path(e.node)
rethrow
elseif e isa TriggerAborted
title = "Failure during trigger execution \
for the combination of components \
{$(join(sort(collect(e.combination); by=T->T.name.name), ", "))}."
subtitle = "This is a bug in the components library."
epilog = render_path(e.node)
rethrow
else
title = "Failure during blueprint addition."
subtitle = "This is a bug in the internal addition procedure.\n"
subtitle = "This is a bug in the internal addition procedure."
epilog = ""
throw
end
throw(ErrorException("\n$(crayon"red")\
raise(ErrorException("\n$(crayon"red")\
âš  âš  âš  $title âš  âš  âš \
$(crayon"reset")\n\
$subtitle\
$subtitle\n\
This system state consistency \
is no longer guaranteed by the program. \
This should not happen and must be considered a bug \
within the components library. \
This should not happen and must be considered a bug. \
Consider reporting if you can reproduce \
with a minimal working example. \
In any case, please drop the current system value \
Expand Down Expand Up @@ -381,6 +432,11 @@ struct ExpansionAborted <: AddException
node::Node
end

struct TriggerAborted <: AddException
node::Node
combination::Set
end

# Once the above have been processed,
# convert into this dedicated user-facing one:
struct AddError{V} <: SystemException
Expand Down
9 changes: 4 additions & 5 deletions src/Framework/blueprints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ function checked_expands_from(bp::Blueprint{V}) where {V}
err()
end
if req isa Component{V}
(typeof(x), reason)
elseif x isa CompType{V}
(x, reason)
(typeof(req), reason)
elseif req isa CompType{V}
(req, reason)
else
err()
end
Expand Down Expand Up @@ -143,8 +143,7 @@ early_check(::Blueprint) = nothing # No particular constraint to enforce by defa
# to feature the provided components.
# This is only called if all component addition conditions are met
# and the above check passed.
# This function must not fail,
# otherwise the system ends up in a bad state.
# This function must not fail or the system may end up in a corrupt state.
# TODO: must it also be deterministic?
# Or can a random component expansion happen
# if infallible and based on consistent blueprint input.
Expand Down
92 changes: 89 additions & 3 deletions src/Framework/component.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@
# This might be implemented in the future for a convincingly motivated need,
# at the cost of extending @component macro to produce abstract types.

# Retrieve type from either instance or the type itself.
component_type(C::CompType) = C
component_type(c::Component) = typeof(c)
component_type(x::Any) =
argerr("Not a component or a component type: $(repr(x)) ::$(typeof(x))")

# Component types being singleton, we *can* infer the value from the type at runtime.
singleton_instance(c::Component) = c
singleton_instance(C::CompType) = throw("No concrete singleton instance of '$C'.")

# Extract underlying system wrapped value type from a component.
system_value_type(::CompType{V}) where {V} = V
system_value_type(::Component{V}) where {V} = V
system_value_type(::CompRef{V}) where {V} = V

#-------------------------------------------------------------------------------------------
# Requirements.
Expand All @@ -51,7 +57,7 @@ requires(c::Component) = requires(typeof(c))
blueprints(C::CompType{V}) where {V} = throw("No blueprint type specified for $C.")
blueprints(c::Component{V}) where {V} = throw("No blueprint type specified for $c.")

#-------------------------------------------------------------------------------------------
# ==========================================================================================
# Conflicts.

# Components that contradict each other can be grouped into mutually exclusive clusters.
Expand Down Expand Up @@ -109,6 +115,86 @@ function all_conflicts(C::CompType)
end)
end

# ==========================================================================================
# Triggers.

# Associate particular components combinations
# with callbacks that will be called immediately after these combinations become available.
# {ValueType => {Component combination => [triggered functions]}}
triggers_(::Type{V}) where {V} = OrderedDict{Set{CompType{V}},OrderedSet{Function}}()
# (same reference/specialization logic as `conflicts_`)

# Setup a trigger.
# The trigger callback signature is either
# trig(::V)
# trig(::V, ::System{V}) # (created if missing)
# and is guaranteed to be called as soon as the given combination of components
# becomes available in the system.
function add_trigger!(components, fn::Function)
V = nothing
first = nothing
set = Set()
for comp in components
C = component_type(comp)

# Guard against inconsistent target values.
if isnothing(V)
V = system_value_type(C)
first = comp
else
aV = system_value_type(C)
aV == V || argerr("Both components for '$V' and '$aV' \
provided within the same trigger: $first and $comp.")
end

# Triangular-check against redundancies.
for already in set
vertical_guard(
C,
already,
() -> argerr("Component $comp specified twice in the same trigger."),
(sub, sup) -> argerr("Both component $sub and its supertype $sup \
specified in the same trigger."),
)
end

push!(set, C)
end

# Guard against inconsistent signatures.
if !hasmethod(fn, Tuple{V,System{V}})
if hasmethod(fn, Tuple{V})
# Append method for consistency.
eval(quote
(::typeof($fn))(v::$V, ::System{$V}) = $fn(v)
end)
else
argerr("Missing expected method on the given trigger function: $fn(::$V).")
end
end

current = triggers_(V) # Creates a new empty value if falling back on default impl.
if isempty(current)
# Specialize to always yield the right reference.
eval(quote
triggers_(::Type{$V}) = $current
end)
end

# Append trigger to this particular combination.
fns = if haskey(current, set)
current[set]
else
current[set] = OrderedSet{Function}()
end
fn in fns && argerr("Function '$fn' already added to triggers for combination \
{$(join(sort(collect(set); by=T->T.name.name), ", "))}.")
push!(fns, fn)

nothing
end
export add_trigger! # Expose directly..

# ==========================================================================================
# Display.

Expand Down
90 changes: 90 additions & 0 deletions test/framework/06-triggers.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Not exactly sure how to best integrate this (late) feature into the other test files.
# Test it separately.
module Triggers

using EcologicalNetworksDynamics.Framework
const F = Framework
export F
using Main: @argfails
using Test

mutable struct Value
_vec::Vector{Symbol}
Value() = new([])
end
Base.copy(v::Value) = deepcopy(v)

@testset "Triggers." begin

# ======================================================================================
# Basic uses.

# (B <: A), (C) and (D)
abstract type A <: Component{Value} end
struct B_b <: Blueprint{Value} end
struct C_b <: Blueprint{Value} end
struct D_b <: Blueprint{Value} end
@blueprint B_b
@blueprint C_b
@blueprint D_b
@component B <: A blueprints(b::B_b)
@component C{Value} blueprints(b::C_b)
@component D{Value} blueprints(b::D_b)

# Setup triggers.
ac_trigger(v::Value) = push!(v._vec, :ac)
ad_trigger(v::Value) = push!(v._vec, :ad)
bc_trigger(v::Value) = push!(v._vec, :bc)
bd_trigger(v::Value) = push!(v._vec, :bd)
add_trigger!([A, C], ac_trigger)
add_trigger!([A, D], ad_trigger)
add_trigger!([B, C], bc_trigger)
add_trigger!([B, D], bd_trigger)

# Nothing happens without combinations.
s = System{Value}()
@test s._value._vec == []
s += B.b()
@test s._value._vec == []
@test System{Value}(C.b(), D.b())._value._vec == []

# Triggers occur in order they were set.
s += C.b()
@test s._value._vec == [:ac, :bc]

s += D.b()
@test s._value._vec == [:ac, :bc, :ad, :bd]

# Get a system hook on-demand.
with_hook(v::Value, ::System) = push!(v._vec, :hook)
add_trigger!([A, C], with_hook) # Okay to have several triggers.
@test System{Value}(B.b(), C.b())._value._vec == [:ac, :hook, :bc] # Still in order.

# ======================================================================================
# Invalid uses.

@argfails(
add_trigger!([A, A], () -> ()),
"Component $A specified twice in the same trigger.",
)

@argfails(
add_trigger!([A, B], () -> ()),
"Both component $_B and its supertype $A specified in the same trigger.",
)

fn() = ()
@argfails(
add_trigger!([A, D], fn),
"Missing expected method on the given trigger function: $fn(::$Value).",
)

@argfails(
add_trigger!([A, C], with_hook),
"Function '$with_hook' already added to triggers for combination {$A, $_C}."
)


end

end

0 comments on commit 07640fe

Please sign in to comment.