From 5a159d530c656926dffcd0becaadd952b3c1b0e2 Mon Sep 17 00:00:00 2001 From: Iago-lito Date: Tue, 30 Jul 2024 14:54:36 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20Start=20fixing=20tests,=20find?= =?UTF-8?q?=20how=20to=20inject=20=5Fhook::System=20with=20@method.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Framework/blueprint_macro.jl | 11 ++- src/Framework/blueprints.jl | 2 +- src/Framework/component.jl | 2 +- src/Framework/component_macro.jl | 9 ++- src/Framework/conflicts_macro.jl | 9 ++- src/Framework/method_macro.jl | 75 ++++++++++++++---- src/Framework/overriding_methods.jl | 117 ++++++++++++++++++++++++++++ test/framework/runtests.jl | 1 - 8 files changed, 203 insertions(+), 23 deletions(-) create mode 100644 src/Framework/overriding_methods.jl diff --git a/src/Framework/blueprint_macro.jl b/src/Framework/blueprint_macro.jl index d25d7b46..7f74da0c 100644 --- a/src/Framework/blueprint_macro.jl +++ b/src/Framework/blueprint_macro.jl @@ -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 @@ -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. @@ -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, @@ -292,7 +298,6 @@ macro blueprint(input...) res end -export @blueprint specified_as_blueprint(B::Type{<:Blueprint}) = false diff --git a/src/Framework/blueprints.jl b/src/Framework/blueprints.jl index 62811a0d..e7662c8f 100644 --- a/src/Framework/blueprints.jl +++ b/src/Framework/blueprints.jl @@ -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. diff --git a/src/Framework/component.jl b/src/Framework/component.jl index 2e174ded..b54f8c81 100644 --- a/src/Framework/component.jl +++ b/src/Framework/component.jl @@ -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. diff --git a/src/Framework/component_macro.jl b/src/Framework/component_macro.jl index e8132aae..23314f67 100644 --- a/src/Framework/component_macro.jl +++ b/src/Framework/component_macro.jl @@ -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 @@ -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) @@ -311,7 +317,6 @@ macro component(input...) res end -export @component #------------------------------------------------------------------------------------------- # The 'conflicts_' mapping entries are either abstract or concrete component, diff --git a/src/Framework/conflicts_macro.jl b/src/Framework/conflicts_macro.jl index f9633aec..21a6d36d 100644 --- a/src/Framework/conflicts_macro.jl +++ b/src/Framework/conflicts_macro.jl @@ -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 @@ -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__,)) | @@ -124,4 +130,3 @@ macro conflicts(input...) res end -export @conflicts diff --git a/src/Framework/method_macro.jl b/src/Framework/method_macro.jl index dcee6690..ebdc38e6 100644 --- a/src/Framework/method_macro.jl +++ b/src/Framework/method_macro.jl @@ -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, ...) = +# +# 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 @@ -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 == :. || @@ -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, ) @@ -305,6 +354,7 @@ macro method(input...) end fn(s._value, args...; _system = s, kwargs...) end + # HERE: how to pass the hook? end, ) @@ -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 diff --git a/src/Framework/overriding_methods.jl b/src/Framework/overriding_methods.jl new file mode 100644 index 00000000..a292b86d --- /dev/null +++ b/src/Framework/overriding_methods.jl @@ -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()) diff --git a/test/framework/runtests.jl b/test/framework/runtests.jl index 544e1a2c..858997c0 100644 --- a/test/framework/runtests.jl +++ b/test/framework/runtests.jl @@ -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__))