Skip to content

Commit

Permalink
🚧 Fixing @method macro tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
iago-lito committed Sep 11, 2024
1 parent 4161a17 commit c044bd1
Show file tree
Hide file tree
Showing 12 changed files with 380 additions and 340 deletions.
57 changes: 5 additions & 52 deletions src/Framework/Framework.jl
Original file line number Diff line number Diff line change
Expand Up @@ -80,69 +80,22 @@
# This *may* make it useless to ever feature component removal.
module Framework

# TODO: Improve ergonomics:
# HERE: Improve ergonomics:
# - [x] Flesh early documentation.
# - [x] Encourage moving sophisticated function definitions outside macro calls
# to ease burden on `Revise`.
# - [x] blueprints *optionnaly* bring other blueprints.
# - [!] Components are not blueprint types, but an autonomous hierachy of singleton types.
# - [.] "*blueprints* imply/bring *blueprints*", not "components imply/bring components"
# - [.] Blueprints 'history' become meaningless if methods can mutate the internal state.
# - [.] Recurring pattern: various blueprints types provide 'the same component': reify.
# - [x] Components are not blueprint types, but an autonomous hierachy of singleton types.
# - [x] "*blueprints* imply/bring *blueprints*", not "components imply/bring components"
# - [x] Blueprints 'history' become meaningless if methods can mutate the internal state.
# - [x] Recurring pattern: various blueprints types provide 'the same component': reify.
# - [ ] `depends(other_method_name)` to inherit all dependent components.
# - [ ] Namespace properties into like system.namespace.namespace.property.
# - [ ] 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.

## HERE: Refactoring design.
## Components are identified by nodes in a type hierarchy,
## and exposed as singleton instances of concrete types in this hierarchy.
## A component may not be a plain marker types,
## and its fields may contain blueprint types for various blueprint providing it.
##
## abstract type Component end
##
## struct _Omega <: Component
## Raw::Type{Blueprint}
## Random::Type{Blueprint}
## Allometry::Type{Blueprint}
## Temperature::Type{Blueprint}
## end
##
## module OmegaBlueprints
## # /!\ many redundant imports to factorize here.
## struct Raw <: Blueprint ... end
## struct Random <: Blueprint ... end
## ...
## end
##
## const Omega = _Omega(
## OmegaBlueprints.Raw,
## OmegaBlueprints.Random,
## ...
## )
## export Omega
##
## function (C::_Omega)(args...; kwargs...)
## if ..
## C.Raw(...)
## elseif ...
## C.Random(...)
## else ...
## end
## end
##
## # Use as a blueprint constructor, but also as a blueprint namespace.
## Omega(...)
## Omega.Random(...)
## Omega.Raw(...)
##
## Components require each other or conflict with each other.
## Blueprints bring each other.
## Blueprints are trees of sub-blueprints and must be treated as such.

using Crayons
using MacroTools
using OrderedCollections
Expand Down
5 changes: 3 additions & 2 deletions src/Framework/macro_helpers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ function to_value(mod, expression, context, error_out, type = nothing)
ev = quote
val = $ev
val isa $type || $error_out(
"$($context): expression does not evaluate to a $($type): \
$($(repr(expression))), but to a $(typeof(val)): $val.",
"$($context): expression does not evaluate to a '$($type)':\n\
Expression: $($(repr(expression)))\n\
Result: $val ::$(typeof(val))",
)
val
end
Expand Down
100 changes: 49 additions & 51 deletions src/Framework/method_macro.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
#
# f(v::Value, ...) = <invoker code>
#
# If no explicit receiver is found,
# then first argument is assumed to be the receiver provided it is untyped or `::Any`.
#
# Then, the macro invokation goes like:
#
# @method f depends(components...) read_as(names...) # or write_as(names...)
Expand Down Expand Up @@ -96,7 +93,7 @@ function method_macro(__module__, __source__, input...)
# unless there is another programmatic way to add a method to a function in Julia?
xp = input[1]
fn_xp, ValueType_xp = nothing, nothing # (hepl JuliaLS)
@capture(xp, fn_xp_{ValueTypeXp_} | fn_xp_)
@capture(xp, fn_xp_{ValueType_xp_} | fn_xp_)
fn_xp isa Symbol ||
fn_xp isa Expr && fn_xp.head == :. ||
perr("Not a method identifier or path: $(repr(fn_xp)).")
Expand All @@ -105,7 +102,7 @@ function method_macro(__module__, __source__, input...)
tovalue(ValueType_xp, "System value type", Type)
push_res!(quote

ValueType = $ValueType_xp # Explicit or uninferred.
ValueType = $ValueType_xp # Explicit or inferred.
fn = $(tovalue(fn_xp, "System method", Function))

end)
Expand Down Expand Up @@ -135,11 +132,11 @@ function method_macro(__module__, __source__, input...)
ValueType = system_value_type(C)
else
if !(C <: Component{ValueType})
actual_V = system_value_type(C)
C_V = system_value_type(C)
xerr("Depends section: system value type
is supposed to be '$ValueType' \
based on the first macro argument, \
but '$C' subtypes '$(Component{actual_V})' \
but '$C' subtypes '$(Component{C_V})' \
and not '$(Component{ValueType})'.")
end
end
Expand All @@ -161,11 +158,11 @@ function method_macro(__module__, __source__, input...)
if Base.isidentifier(kw)
if !(kw in (read_kw, write_kw))
perr("Invalid section keyword: $(repr(kw)). \
Expected `$read_kw` or `$write_kw` or `depends`.")
Expected :$read_kw or :$write_kw or :depends.")
end
if !isnothing(proptype)
proptype == kw && perr("The `$kw` section is specified twice.")
perr("Cannot specify both `$proptype` section and `$kw`.")
proptype == kw && perr("The :$kw section is specified twice.")
perr("Cannot specify both :$proptype section and :$kw.")
end
for pname in propnames
pname isa Symbol ||
Expand All @@ -179,7 +176,7 @@ function method_macro(__module__, __source__, input...)

perr("Unexpected @method section. \
Expected `depends(..)`, `$read_kw(..)` or `$write_kw(..)`. \
Got: $(repr(i)).")
Got instead: $(repr(i)).")

end

Expand All @@ -191,7 +188,7 @@ function method_macro(__module__, __source__, input...)
push_res!(
quote
isnothing(ValueType) && xerr("The system value type cannot be inferred \
when no dependencies are given.
when no dependencies are given.\n\
Consider making it explicit \
with the first macro argument: \
`$fn{MyValueType}`.")
Expand Down Expand Up @@ -224,8 +221,14 @@ function method_macro(__module__, __source__, input...)
values = Set()
system_values = Set()
systems_only = Set()
for (p, n) in zip(parms, names)
for (i, (p, n)) in enumerate(zip(parms, names))
p isa Core.TypeofVararg && continue
if n == Symbol("#unused#")
# May become used in the generated method
# if it turns out to be a receiver.
n = Symbol('#', i)
names[i] = n
end
p <: ValueType && push!(values, n)
p <: System{ValueType} && push!(system_values, n)
p === System && push!(systems_only, n)
Expand All @@ -234,15 +237,11 @@ function method_macro(__module__, __source__, input...)
(what, set, type) ->
xerr("Receiving several (possibly different) $what \
is not yet supported by the framework. \
Here both :$(pop!(set)) and :$(pop!(set)) \
Here both parameters :$(pop!(set)) and :$(pop!(set)) \
are of type $type.")
length(values) > 1 && xerr("system/values parameters", values, ValueType)
receiver = if isempty(values)
parms[1] === Any || continue
names[1]
else
pop!(values)
end
length(values) > 1 && severr("system/values parameters", values, ValueType)
isempty(values) && continue
receiver = pop!(values)
sv = system_values
length(sv) > 1 && severr("system hooks", sv, System{ValueType})
hook = if isempty(system_values)
Expand All @@ -259,8 +258,7 @@ function method_macro(__module__, __source__, input...)
isempty(to_wrap) &&
xerr("No suitable method has been found to mark $fn as a system method. \
Valid methods must have at least \
one 'receiver' argument of type ::$ValueType \
or a first ::Any argument to be implicitly considered as such.")
one 'receiver' argument of type ::$ValueType.")
end,
)

Expand Down Expand Up @@ -294,34 +292,32 @@ function method_macro(__module__, __source__, input...)
try
which(fn, Tuple{ValueType})
catch
xerr("The function '$fn' cannot be called \
xerr("The function cannot be called \
with exactly 1 argument of type '$ValueType' \
as required to be set as a 'read' property.")
end
end)
end

if kw == write_kw
push_res!(
quote
# The assessment is trickier here
# since there may be a constraint on the second argument,
# although there is no restriction which constraint,
# so which(fn, Tuple{ValueType,Any}) would incorrectly fail.
# The best I've found then is to scroll all methods
# until we find a match for the desired signature.
# Maybe there is a better way to do this in julia?
match = any(Iterators.map(methods(fn)) do m
parms = m.sig.parameters
length(parms) == 3 && ValueType <: parms[2]
end)
if !match
xerr("The function '$fn' cannot be called with exactly 2 arguments, \
the first one being of type '$ValueType', \
as required to be set as a 'write' property.")
end
end,
)
push_res!(quote
# The assessment is trickier here
# since there may be a constraint on the second argument,
# although there is no restriction which constraint,
# so which(fn, Tuple{ValueType,Any}) would incorrectly fail.
# The best I've found then is to scroll all methods
# until we find a match for the desired signature.
# Maybe there is a better way to do this in julia?
match = any(Iterators.map(methods(fn)) do m
parms = m.sig.parameters
length(parms) == 3 && ValueType <: parms[2]
end)
if !match
xerr("The function cannot be called with exactly 2 arguments, \
the first one being of type '$ValueType', \
as required to be set as a 'write' property.")
end
end)
end

# Check properties availability.
Expand All @@ -331,19 +327,19 @@ function method_macro(__module__, __source__, input...)
quote
for psymbol in $[propsymbols...]
has_read_property(ValueType, Val(psymbol)) &&
xerr("The property $psymbol is already defined \
xerr("The property :$psymbol is already defined \
for $($System){$ValueType}.")
end
end
else
quote
for psymbol in $[propsymbols...]
has_read_property(ValueType, Val(psymbol)) ||
xerr("The property $psymbol cannot be marked 'write' \
xerr("The property :$psymbol cannot be marked 'write' \
without having first been marked 'read' \
for $($System){$ValueType}.")
has_write_property(ValueType, Val(psymbol)) &&
xerr("The property $psymbol is already marked 'write' \
xerr("The property :$psymbol is already marked 'write' \
for $($System){$ValueType}.")
end
end
Expand Down Expand Up @@ -379,7 +375,7 @@ function method_macro(__module__, __source__, input...)
# when the macro is called within @testset blocks
# with LOCAL_MACROCALLS = true:
# https://stackoverflow.com/a/55292662/3719101)
$dep = missing_dependency_for($($efn), $receiver)
$dep = first_missing_dependency_for($($efn), $receiver)
if !isnothing($dep)
$a = isabstracttype($dep) ? " a" : ""
throw(
Expand Down Expand Up @@ -433,9 +429,11 @@ function method_macro(__module__, __source__, input...)
end

# Record as specified to avoid it being recorded again.
push_res!(quote
Framework.specified_as_method(::Type{ValueType}, ::typeof(fn)) = true
end)
push_res!(
quote
Framework.specified_as_method(::Type{ValueType}, ::Type{typeof(fn)}) = true
end,
)

# Avoid confusing/leaky return type from macro invocation.
push_res!(quote
Expand Down
9 changes: 5 additions & 4 deletions src/Framework/methods.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# - method!(v::Value, rhs)
#
# .. can optionally become properties of the system/value,
# in the sense of julia's `getproperty/set_property`.
# in the sense of julia's `getproperty/set_property!`.
#
# The properties also come in two styles:
#
Expand All @@ -33,7 +33,7 @@ missing_dependencies_for(fn::Type{<:Function}, s::System{V}) where {V} =
!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)
function first_missing_dependency_for(fn::Type{<:Function}, s::System)
for dep in missing_dependencies_for(fn, s)
return dep
end
Expand All @@ -43,7 +43,8 @@ end
# Direct call with the functions themselves.
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)
first_missing_dependency_for(fn::Function, s::System) =
first_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 Expand Up @@ -137,6 +138,6 @@ struct PropertyError{V} <: SystemException
PropertyError(::Type{V}, s, m) where {V} = new{V}(s, m, PhantomData{V}())
end
function Base.showerror(io::IO, e::PropertyError{V}) where {V}
println(io, "In property '$(e.name)' of '$V': $(e.message)")
println(io, "In property :$(e.name) of '$V': $(e.message)")
end
properr(V, n, m) = throw(PropertyError(V, n, m))
8 changes: 4 additions & 4 deletions src/Framework/system.jl
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ 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(fn, system)
miss = first_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.")
Expand All @@ -87,7 +87,7 @@ 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(fn, system)
miss = first_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.")
Expand Down Expand Up @@ -166,7 +166,7 @@ export properties
# Yields (:propname, read, Option{write})
function properties(s::System{V}) where {V}
imap(ifilter(properties(V)) do (_, read, _, _)
isnothing(missing_dependency_for(read, s))
isnothing(first_missing_dependency_for(read, s))
end) do (name, read, write, _)
(name, read, write)
end
Expand All @@ -177,7 +177,7 @@ end
# 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))
!isnothing(first_missing_dependency_for(read, s))
end) do (name, read, write, deps)
(name, read, write, ifilter(d -> !has_component(s, d), deps))
end
Expand Down
Loading

0 comments on commit c044bd1

Please sign in to comment.