Skip to content

Commit

Permalink
🚧 Start fixing tests, find how to inject _hook::System with @method.
Browse files Browse the repository at this point in the history
  • Loading branch information
iago-lito committed Jul 30, 2024
1 parent 566f101 commit 5a159d5
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 23 deletions.
11 changes: 8 additions & 3 deletions src/Framework/blueprint_macro.jl
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ export Brought
# The code checking macro invocation consistency requires
# that pre-requisites (methods implementations) be specified *prior* to invocation.
macro blueprint(input...)
blueprint_macro(__module__, __source__, input...)
end
export @blueprint

# Extract function to ease debugging with Revise.
function blueprint_macro(__module__, __source__, input...)

# Push resulting generated code to this variable.
res = quote end
Expand Down Expand Up @@ -120,7 +126,7 @@ macro blueprint(input...)
fieldtype <: BroughtField || continue
C = componentof(fieldtype)
TC = Type{C}
applicable(implied_blueprint_for, (NewBlueprint, TC)) ||
hasmethod(implied_blueprint_for, Tuple{NewBlueprint,TC}) ||
xerr("Method $implied_blueprint_for($NewBlueprint, $TC) unspecified.")

# Triangular-check against redundancies.
Expand Down Expand Up @@ -155,7 +161,7 @@ macro blueprint(input...)
imap = Iterators.map
ifilter = Iterators.filter
Framework.brought(b::NewBlueprint) =
imap(ifilter(!isnothing, imap(f -> getfield(b, f), keys(brought)))) do f
imap(ifilter(!isnothing, imap(f -> getfield(b, f), keys(broughts)))) do f
f isa Component ? typeof(f) : f
end
end,
Expand Down Expand Up @@ -292,7 +298,6 @@ macro blueprint(input...)

res
end
export @blueprint

specified_as_blueprint(B::Type{<:Blueprint}) = false

Expand Down
2 changes: 1 addition & 1 deletion src/Framework/blueprints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Base.copy(b::Blueprint) = deepcopy(b)
# Return non-empty list if components are required
# for this blueprint to expand,
# even though the corresponding component itself would make sense without these.
expands_from(B::Blueprint{V}) where {V} = throw("Unspecified requirements for $B.")
expands_from(::Blueprint{V}) where {V} = () # Require nothing by default.
# The above is specialized by hand by framework users,
# so make its return type flexible,
# guarded by the below.
Expand Down
2 changes: 1 addition & 1 deletion src/Framework/component.jl
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const Reason = Option{String}
const CompsReasons{V} = OrderedDict{CompType{V},Reason}

# Specify which components are needed for the focal one to make sense.
requires(C::CompType{V}) where {V} = throw("Unspecified requirements for $C.")
requires(::CompType{V}) where {V} = () # Require nothing by default.
requires(c::Component) = requires(typeof(c))

# List all possible blueprints types providing the component.
Expand Down
9 changes: 7 additions & 2 deletions src/Framework/component_macro.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@
# The code checking macro invocation consistency requires
# that these pre-requisites be specified *prior* to invocation.
macro component(input...)
component_macro(__module__, __source__, input...)
end
export @component

# Extract function to ease debugging with Revise.
function component_macro(__module__, __source__, input...)

# Push resulting generated code to this variable.
res = quote end
Expand Down Expand Up @@ -276,7 +282,7 @@ macro component(input...)
push_res!(quote
for (_, B) in base_blueprints
$__module__.eval(quote
$Framework.componentsof(::$B) = $($etys,)
$Framework.componentsof(::$B) = $($ety,)
end)
end
end)
Expand Down Expand Up @@ -311,7 +317,6 @@ macro component(input...)

res
end
export @component

#-------------------------------------------------------------------------------------------
# The 'conflicts_' mapping entries are either abstract or concrete component,
Expand Down
9 changes: 7 additions & 2 deletions src/Framework/conflicts_macro.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
#
# Minimal use: @conflicts(A, B, C)
macro conflicts(input...)
conflicts_macro(__module__, __source__, input...)
end
export @conflicts

# Extract function to ease debugging with Revise.
function conflicts_macro(__module__, __source__, input...)

# Push resulting generated code to this variable.
res = quote end
Expand Down Expand Up @@ -52,7 +58,7 @@ macro conflicts(input...)
entries = :([])
for entry in input

comp, conf, invalid, reasons, mess = repeat(nothing, 5) # (help JuliaLS)
comp, conf, invalid, reasons, mess = repeat([nothing], 5) # (help JuliaLS)
#! format: off
@capture(entry,
(comp_ => (reasons__,)) |
Expand Down Expand Up @@ -124,4 +130,3 @@ macro conflicts(input...)
res

end
export @conflicts
75 changes: 62 additions & 13 deletions src/Framework/method_macro.jl
Original file line number Diff line number Diff line change
@@ -1,17 +1,53 @@
# Convenience macro for defining methods and properties.
#
# Invoker defines the behaviour in a function code and then calls:
# Invoker defines the behaviour in a function code containing at least one 'receiver':
# an argument typed with the system wrapped value.
#
# @method function_name depends(components...) read_as(names...) # or write_as(names...)
# f(v::Value, ...) = <invoker code>
#
# If no receiver is found, the first argument is assumed to be it if it's `::Any`.
#
# Then, the macro invokation goes like:
#
# @method f depends(components...) read_as(names...) # or write_as(names...)
#
# Or alternately:
#
# @method begin
# function_name
# function_name # or function_name::ValueType if inference fails. HERE: implement.
# depends(components...)
# read_as(property_names...) # or write_as(property_names...)
# end
#
# This will generate additional methods to `f` so it accepts `System{Value}` instead of
# `Value` as the receiver. These method check that components dependencies are met
# before forwarding to the original method.
#
# If an original method has the exact:
# - `f(receiver)` signature, then it can be marked as a `read` property.
# - `f(receiver, rhs)` signature, then it can be marked as a `write` property.
#
# Sometimes, the method needs to take decision
# depending on other system components that are not strict dependencies,
# so the whole system needs to be queried and not just the wrapped value.
# In this case, the invoker may add a `::System` parameter to their signature:
#
# f(v, a, b, _system::System) = ...
#
# The new method generated will then elide this extra argument,
# yet forward the whole system to it:
#
# f(v::System{ValueType}, a, b) = f(v._value, a, b, v) # (generated)
#
# HERE: implement from the toy sandox: ./overriding_methods.jl
#
macro method(input...)
method_macro(__module__, __source__, input...)
end
export @method

# Extract function to ease debugging with Revise.
function method_macro(__module__, __source__, input...)

# Push resulting generated code to this variable.
res = quote end
Expand Down Expand Up @@ -55,9 +91,8 @@ macro method(input...)

# The first section needs to specify the function containing adequate behaviour code.
# Since an additional method to this function needs to be generated,
# then the input expression must be a plain symbol or Path.To.symbol,
# unless there is another programmatic way
# to add a method to a function in julia.
# then the input expression must be a plain symbol or Path.To.symbol..
# unless there is another programmatic way to add a method to a function in Julia?
fn_xp = input[1]
fn_xp isa Symbol ||
fn_xp isa Expr && fn_xp.head == :. ||
Expand Down Expand Up @@ -246,11 +281,25 @@ macro method(input...)
# Check properties availability.
propsymbols = map(s -> first(s.args), propsymbols)
push_res!(
quote
for psymbol in $[propsymbols...]
has_read_property(ValueType, psymbol) && xerr(
"The property $psymbol is already defined for $($System){$Valuetype}.",
)
if kw == read_kw
quote
for psymbol in $[propsymbols...]
has_read_property(ValueType, psymbol) &&
xerr("The property $psymbol is already defined \
for $($System){$ValueType}.")
end
end
else
quote
for psymbol in $[propsymbols...]
has_read_property(ValueType, psymbol) ||
xerr("The property $psymbol cannot be marked 'write' \
without having first been marked 'read' \
for $($System){$ValueType}.")
has_write_property(ValueType, psymbol) &&
xerr("The property $psymbol is already marked 'write' \
for $($System){$ValueType}.")
end
end
end,
)
Expand Down Expand Up @@ -305,6 +354,7 @@ macro method(input...)
end
fn(s._value, args...; _system = s, kwargs...)
end
# HERE: how to pass the hook?
end,
)

Expand All @@ -331,7 +381,6 @@ macro method(input...)

res
end
export @method

# Check whether the function has already been specified as a @method.
specified_as_method(::Type, ::Type{Function}) = false
specified_as_method(::Type, ::Type{<:Function}) = false
117 changes: 117 additions & 0 deletions src/Framework/overriding_methods.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# HERE: a working snippet to be integrated into @method.

struct Value end
struct System{V}
_value::V
end

# Lib.
f(a::Value, b, args...; c=5, kwargs...) = a + b - c
g(a::Value, b, _s::System, args...; c=5, kwargs...) = a + b - c

# Framework.
# f(a::System, b, args...; c = 5, kwargs...) = f(a._value, b, args...; c = 5, kwargs...)
# g(a::System, b, _s::System, args...; c = 5, kwargs...) = g(a._value, b, _s, args...; c = 5, kwargs...)

ValueType = Value
fn_xp = :f
fn = eval(fn_xp)
__module__ = Main
efn = :($__module__.$fn_xp)
xerr = (mess) -> throw(mess)

to_override = []
for mth in methods(fn)

# Retrieve fixed-parameters types for the method.
parms = collect(mth.sig.parameters[2:end])
isempty(parms) && continue
# Retrieve their names.
# https://discourse.julialang.org/t/get-the-argument-names-of-an-function/32902/4?u=iago-lito
pnames = ccall(:jl_uncompress_argnames, Vector{Symbol}, (Any,), mth.slot_syms)[2:end]

# Among them, find which one to use as the system "receiver":
# either the only one typed with 'ValueType',
# or the first one if 'Any'.
# Take this opportunity to also look for a single parameter
# typed with `System` or `System{ValueType}`.
# If present, the overriding method will pass
# a reference to the global system through it.
values = Set()
system_values = Set()
systems_only = Set()
for (name, p) in zip(pnames, parms)
p isa Core.TypeofVararg && continue
p <: ValueType && push!(values, name)
p <: System{ValueType} && push!(system_values, name)
p === System && push!(systems_only, name)
end
length(values) > 1 && xerr("Receiving several (possibly different) system/values \
is not yet supported by the framework. \
Here both :$(pop!(values)) and :$(pop!(values)) \
are of type $ValueType.")
receiver = if isempty(values)
parms[1] === Any || continue
pnames[1]
else
pop!(values)
end
sv = system_values
length(sv) > 1 && xerr("Receiving several (possibly different) system hooks \
is not yet supported by the framework. \
Here both :$(pop!(sv)) and :$(pop!(sv)) \
are of type $(System{ValueType}).")
hook = if isempty(system_values)
so = systems_only
length(so) > 1 && xerr("Receiving several (possibly different) system hooks \
is not yet supported by the framework. \
Here both :$(pop!(so)) and :$(pop!(so)) \
are of type $System.")
isempty(so) ? nothing : pop!(so)
else
pop!(system_values)
end

# Record for overriding.
push!(to_override, (mth, parms, pnames, receiver, hook))
end
isempty(to_override) && xerr("No suitable method has been found to mark $fn as a system method. \
Valid methods must have at least one argument of type ::$ValueType \
or a first ::Any argument to be implicitly considered as such.")

for (mth, parms, pnames, receiver, hook) in to_override
xp = quote
function $efn(; kwargs...)
$efn(; kwargs...)
end
end
xp_parms = xp.args[2].args[1].args
xp_args = xp.args[2].args[2].args[3].args
for (name, type) in zip(pnames, parms)
parm, arg = if type isa Core.TypeofVararg
(:($name::$(type.T)...), :($name...))
else
if name == receiver
# Dispatch on the system to transmit the inner value.
(:($name::System{$type}), :($name._value))
elseif name == hook
# Don't receive at all, but transmit from the receiver.
(nothing, receiver)
else
# Other arguments are just forwarded.
(:($name::$type), name)
end
end
isnothing(parm) || push!(xp_parms, parm)
push!(xp_args, arg)
end
print(xp)

eval(xp)
any = true

end


# User.
s = System{Value}(Value())
1 change: 0 additions & 1 deletion test/framework/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ 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 Down

0 comments on commit 5a159d5

Please sign in to comment.