Skip to content

Commit

Permalink
🚧 Factorize field assignment + constructor + conversion with implicit…
Browse files Browse the repository at this point in the history
… brought field constructor.
  • Loading branch information
iago-lito committed Sep 4, 2024
1 parent c1bd875 commit b383518
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 88 deletions.
146 changes: 97 additions & 49 deletions src/Framework/blueprint_macro.jl
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ function blueprint_macro(__module__, __source__, input...)
ferr(m) = throw(
BroughtAssignFailure{ValueType}(NewBlueprint, prop, expected_C, m, rhs),
)
val = if rhs isa Blueprint
BF = BroughtField{expected_C,ValueType}
bf = if rhs isa Blueprint
V = system_value_type(rhs)
V == ValueType || ferr("Expected a RHS blueprint for $ValueType.\n\
Got instead a blueprint for: $V.")
Expand All @@ -219,57 +220,29 @@ function blueprint_macro(__module__, __source__, input...)
ferr("Blueprint would instead expand into $comps.")
C = first(comps)
C <: expected_C || ferr("Blueprint would instead expand into $C.")
rhs
BF(rhs)
elseif rhs isa Component || rhs isa CompType
rhs isa Component && (rhs = typeof(rhs))
V = system_value_type(rhs)
V == ValueType || ferr("Expected a RHS component for $ValueType.\n\
Got instead a component for: $V.")
rhs === expected_C || ferr("Value would imply instead: $rhs.")
expected_C
BF(expected_C)
elseif isnothing(rhs)
nothing
BF(nothing)
else
# In any other case, forward to an underlying blueprint constructor.
# TODO: make this constructor customizeable depending on the value.
cstr =
isabstracttype(expected_C) ? expected_C :
singleton_instance(expected_C)
# This needs to be callable.
isempty(methods(cstr)) && ferr("'$cstr' is not (yet?) callable. \
Consider providing a \
blueprint value instead.")
args, kwargs = if rhs isa Tuple{<:Tuple,<:NamedTuple}
rhs
elseif rhs isa Tuple
(rhs, (;))
elseif rhs isa NamedTuple
((), rhs)
else
((rhs,), (;))
# (happens via conversion so the same logic
# applies to host blueprint constructor arguments)
try
Base.convert(BF, rhs)
catch e
e isa BroughtConvertFailure &&
rethrow(BroughtAssignFailure(NewBlueprint, prop, e))
rethrow(e)
end
bp = cstr(args...; kwargs...)
# It is a bug in the component library (introduced by framework users)
# if the implicit constructor yields a wrong value.
function bug(m)
red, res = (crayon"bold red", crayon"reset")
ferr("Implicit blueprint constructor $m\n\
$(red)This is a bug in the components library.$res")
end
bp isa Blueprint || bug("did not yield a blueprint, \
but: $(repr(bp)) ::$(typeof(bp)).")
V = system_value_type(bp)
V == ValueType || bug(
"did not yield a blueprint for '$ValueType', but for '$V': $bp.",
)
comps = componentsof(bp)
length(comps) == 1 ||
bug("yielded instead a blueprint for: $comps.")
C = first(comps)
C == expected_C || bug("yielded instead a blueprint for: $C.")
bp
end
setfield!(b, prop, BroughtField{expected_C,ValueType}(val))
setfield!(b, prop, bf)
end
end,
)
Expand Down Expand Up @@ -329,6 +302,9 @@ function blueprint_macro(__module__, __source__, input...)
res
end

#-------------------------------------------------------------------------------------------
# Minor stubs for the macro to work.

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

# Stubs for display methods.
Expand All @@ -349,21 +325,72 @@ function provided_comps_display(bp::Blueprint)
end
end

#-------------------------------------------------------------------------------------------
# Error when assigning to brought blueprint fields.
struct BroughtAssignFailure{V}
BlueprintType::Type{<:Blueprint{V}}
fieldname::Symbol
# Checked call to implicit constructor, supposed to yield a consistent blueprint.
function implicit_constructor_for(
expected_C::CompType,
ValueType::DataType,
args::Tuple,
kwargs::NamedTuple,
rhs::Any,
)
err(m) = throw(BroughtConvertFailure{ValueType}(expected_C, m, rhs))
# TODO: make this constructor customizeable depending on the value.
cstr = isabstracttype(expected_C) ? C : singleton_instance(expected_C)
# This needs to be callable.
isempty(methods(cstr)) && err("'$cstr' is not (yet?) callable. \
Consider providing a \
blueprint value instead.")
bp = cstr(args...; kwargs...)
# It is a bug in the component library (introduced by framework users)
# if the implicit constructor yields a wrong value.
function bug(m)
red, res = (crayon"bold red", crayon"reset")
err("Implicit blueprint constructor $m\n\
$(red)This is a bug in the components library.$res")
end
bp isa Blueprint || bug("did not yield a blueprint, \
but: $(repr(bp)) ::$(typeof(bp)).")
V = system_value_type(bp)
V == ValueType || bug("did not yield a blueprint for '$ValueType', but for '$V': $bp.")
comps = componentsof(bp)
length(comps) == 1 || bug("yielded instead a blueprint for: $comps.")
C = first(comps)
C == expected_C || bug("yielded instead a blueprint for: $C.")
bp
end

# Error when implicitly using default constructor
# to convert arbitrary input to a brought field.
struct BroughtConvertFailure{V}
BroughtComponent::CompType{V}
message::String
rhs::Any
end

# Specialize error when it occurs in the context
# of a field assignment on the host blueprint.
struct BroughtAssignFailure{V}
HostBlueprint::Type{<:Blueprint{V}}
fieldname::Symbol
fail::BroughtConvertFailure{V}
end

function Base.showerror(io::IO, e::BroughtConvertFailure)
(; BroughtComponent, message, rhs) = e
print(
io,
"Failed to convert input \
to a brought blueprint for $BroughtComponent:\n$message\n\
Input was: $(repr(rhs)) ::$(typeof(rhs))",
)
end

function Base.showerror(io::IO, e::BroughtAssignFailure)
(; BlueprintType, fieldname, BroughtComponent, message, rhs) = e
(; HostBlueprint, fieldname, fail) = e
(; BroughtComponent, message, rhs) = fail
print(
io,
"Failed to assign to field :$fieldname of '$BlueprintType' \
"Failed to assign to field :$fieldname of '$HostBlueprint' \
supposed to bring component $BroughtComponent:\n$message\n\
RHS was: $(repr(rhs)) ::$(typeof(rhs))",
)
Expand Down Expand Up @@ -392,6 +419,18 @@ function Base.convert(::Type{BroughtField{C,V}}, bp::Blueprint{V}) where {C,V}
BroughtField{C,V}(bp)
end

# From arguments to embed with a call to implicit constructor.
function Base.convert(BF::Type{BroughtField{C,V}}, args::Tuple) where {C,V}
BF(implicit_constructor_for(C, V, args, (;), args))
end
function Base.convert(BF::Type{BroughtField{C,V}}, kwargs::NamedTuple) where {C,V}
BF(implicit_constructor_for(C, V, (), kwargs, kwargs))
end
function Base.convert(BF::Type{BroughtField{C,V}}, akw::Tuple{Tuple,NamedTuple}) where {C,V}
args, kwargs = akw
BF(implicit_constructor_for(C, V, args, kwargs, akw))
end

# From anything else to disallow.
Base.convert(::Type{BroughtField{C,V}}, i::Any) where {C,V} =
throw(InvalidBroughtInput(i, C))
Expand Down Expand Up @@ -441,6 +480,15 @@ end

#-------------------------------------------------------------------------------------------

function Base.show(io::IO, ::Type{<:BroughtField{C,V}}) where {C,V}
grey = crayon"black"
reset = crayon"reset"
print(io, "$grey<brought field type for $reset$C$grey>$reset")
end
Base.show(io::IO, bf::BroughtField) = display_blueprint_field_short(io, bf)
Base.show(io::IO, ::MIME"text/plain", bf::BroughtField) =
display_blueprint_field_long(io, bf)

function display_blueprint_field_short(io::IO, bf::BroughtField)
grey = crayon"black"
reset = crayon"reset"
Expand Down Expand Up @@ -468,7 +516,7 @@ function display_blueprint_field_long(io::IO, bf::BroughtField; level = 0)
elseif value isa CompType
print(io, "$grey<implied blueprint for $reset$value$grey>$reset")
elseif value isa Blueprint
print(io, "$grey<brought $reset")
print(io, "$grey<embedded $reset")
display_long(io, value; level)
print(io, "$grey>$reset")
else
Expand Down
83 changes: 44 additions & 39 deletions test/framework/02-blueprint_macro.jl
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,18 @@ end

# Embed using implicit blueprint construction.
(::typeof(Ejp))(u, v) = Ejp.b(u, v)

# As field assignments.
bdz.ejp = (15, 30)
bdz.ejp # HERE: <- improve brought blueprint display.
s = S(bdz)
@test comps(s) == [Xhu, Ejp, Bdz]
@test (s.u, s.v) == (15, 30)

# As constructor arguments.
bdz = Bdz_b(1, 2, nothing, (15, 30))
s = S(bdz)
@test comps(s) == [Ejp, Bdz]
@test (s.u, s.v) == (15, 30)

# HERE: the working versions.

Expand All @@ -234,7 +243,10 @@ end
@blueprint Amg_b
@failswith(Brought(Amg_b), MethodError)

# Forget to specify how to construct implied blueprints.
#---------------------------------------------------------------------------------------
# Implied blueprint constructors.

# Forgot to define it.
struct Ihb_b <: Blueprint{Value}
x::Float64
y::Float64
Expand Down Expand Up @@ -276,7 +288,7 @@ end
Consider removing either one."
)

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#---------------------------------------------------------------------------------------
# Guard against redundant blueprints.
struct Qev_b <: Blueprint{Value}
data::Brought(Ejp)
Expand Down Expand Up @@ -309,7 +321,10 @@ end
is also specified as <TopComp>."
)

# Can't call the implicit embedded blueprint constructor if undefined.
#---------------------------------------------------------------------------------------
# Implicit brought blueprints constructor.

# Can't call if undefined.
struct Opv_b <: Blueprint{Value} end
@blueprint Opv_b
@component Opv{Value} blueprints(b::Opv_b)
Expand All @@ -321,62 +336,52 @@ end
@component Twt{Value} blueprints(b::Twt_b)
twt = Twt_b(nothing)

baf(m, rhs) = F.BroughtAssignFailure(Twt_b, :opv, _Opv, m, rhs)
@failswith((twt.opv = ()), baf(
"'$Opv' is not (yet?) callable. \
Consider providing a blueprint value instead.",
(),
))
bcf(m, rhs) = F.BroughtConvertFailure(_Opv, m, rhs)
baf(m, rhs) = F.BroughtAssignFailure(Twt_b, :opv, bcf(m, rhs))

# The implicit constructor must construct a consistent blueprint.
constructed = nothing # (to change without triggering 'WARNING: Method definition overwritten'.
# Check both failure on constructor and on field assignment.
erm = "'$Opv' is not (yet?) callable. \
Consider providing a blueprint value instead."
@failswith(Twt_b(()), bcf(erm, ()))
@failswith((twt.opv = ()), baf(erm, ()))

# Must construct a consistent blueprint.
constructed = nothing
# (change ↑ freely to test without triggering 'WARNING: Method definition overwritten')
(::typeof(Opv))() = constructed
red, res = (crayon"bold red", crayon"reset")
bug = "\n$(red)This is a bug in the components library.$res"

constructed = 5
@failswith(
(twt.opv = ()),
baf(
"Implicit blueprint constructor did not yield a blueprint, but: 5 ::$Int.$bug",
(),
)
)
erm = "Implicit blueprint constructor did not yield a blueprint, but: 5 ::$Int.$bug"
@failswith(Twt_b(()), bcf(erm, ()))
@failswith((twt.opv = ()), baf(erm, ()))

struct Yfi_b <: Blueprint{Int} end
constructed = Yfi_b()
@failswith(
(twt.opv = ()),
baf(
"Implicit blueprint constructor did not yield a blueprint for '$Value', \
but for '$Int': $Yfi_b().$bug",
(),
)
)
erm = "Implicit blueprint constructor did not yield a blueprint for '$Value', \
but for '$Int': $Yfi_b().$bug"
@failswith(Twt_b(()), bcf(erm, ()))
@failswith((twt.opv = ()), baf(erm, ()))

struct Sxo_b <: Blueprint{Value} end
@blueprint Sxo_b
@component Iej{Value}
@component Axl{Value}
F.componentsof(::Sxo_b) = [Iej, Axl]
constructed = Sxo_b()
@failswith((twt.opv = ()), baf(
"Implicit blueprint constructor yielded instead \
a blueprint for: $([Iej, Axl]).$bug",
(),
))
erm = "Implicit blueprint constructor yielded instead \
a blueprint for: $([Iej, Axl]).$bug"
@failswith(Twt_b(()), bcf(erm, ()))
@failswith((twt.opv = ()), baf(erm, ()))

struct Dpt_b <: Blueprint{Value} end
@blueprint Dpt_b
@component Dpt{Value} blueprints(b::Dpt_b)
constructed = Dpt_b()
@failswith(
(twt.opv = ()),
baf(
"Implicit blueprint constructor yielded instead a blueprint for: <Dpt>.$bug",
(),
)
)
erm = "Implicit blueprint constructor yielded instead a blueprint for: <Dpt>.$bug"
@failswith(Twt_b(()), bcf(erm, ()))
@failswith((twt.opv = ()), baf(erm, ()))

# HERE: the failing versions.

Expand Down

0 comments on commit b383518

Please sign in to comment.