Propagatable: private API
Utility functions
PortfolioOptimisers.PROP_TAG_NAMES — Constant
PROP_TAG_NAMESThe propagation tag set of @propagatable, as data.
One entry per field tag. The recognition layer is derived from this tuple rather than spelled out per tag: the macro names (PROP_TAG_MACRO_NAMES), the lookup (prop_tag), the gate (is_prop_tag_call), the peeler (peel_prop_tags) and the parser (propagatable_parse_body) all read it. A new propagation channel is one row here, one branch in prop_tag_expr, one entry in PROP_TAG_CHANNELS and one stub macro; check_prop_tag_macros refuses to load the module when a row lacks any of the three.
Related
PortfolioOptimisers.PROP_TAG_MACRO_NAMES — Constant
PROP_TAG_MACRO_NAMESThe macro name of every tag of PROP_TAG_NAMES, in the same order.
Derived from the tag names, so a row of the table carries no second spelling. prop_tag matches a :macrocall head against this tuple.
Algorithm
- Map each tag of
PROP_TAG_NAMEStoSymbol("@", tag), which is the name Julia gives that tag's macro.
The map preserves the order, so the $k$-th entry here is the macro name of the $k$-th tag there. prop_tag and check_prop_tag_macros both walk the two tuples together, and that pairing is what the shared order guarantees.
Related
PortfolioOptimisers.PROP_TAG_CHANNELS — Constant
PROP_TAG_CHANNELSThe propagation channels of @propagatable, with their tag precedence as data.
One entry per generated method. Each entry has two tuples of tags:
gate: the tags that make@propagatableemit the method at all.precedence: the order in which a field's tags are consulted. The first tag of this tuple that the field carries decides the field's transform; the rest are ignored for that channel.
The factory channel prefers @fprop over @wprop. The prior channel prefers @pprop, then @cprop, then @wprop, then @fprop, so @pprop wins over @fprop on one field. The precedence used to live in two hand-written if/elseif chains that no comment linked; it is now read by prop_channel_pairs for every channel.
A tag means what its channel says it means. The obs channel reads the same @wprop and @fprop tags as factory and gives them different transforms: factory replaces a @wprop field with an incoming ObsWeights value, while obs indexes the value already there. This is why prop_tag_expr takes the channel as well as the tag. It also means a weights field opts into obs_weights_view by carrying @wprop, with no second tag to write and no second tag to forget.
Related
PortfolioOptimisers.PROPAGATABLE_CONTRACTS — Constant
PROPAGATABLE_CONTRACTSEvery type declared with @propagatable, paired with its @pprop-tagged field names.
One entry is appended by the macro itself, immediately after the struct it declares, so the list is complete by the time the module finishes loading — including types declared in external packages. check_propagatable_contracts is what reads it.
Related
PortfolioOptimisers.prop_tag — Function
prop_tag(x) -> Union{Nothing, Symbol}
Return the tag of PROP_TAG_NAMES that x names, or nothing.
x is the head of a :macrocall node. It is a bare Symbol in a struct body written by hand, and a GlobalRef once another macro has expanded around it, so both spellings resolve. A name outside PROP_TAG_MACRO_NAMES gives nothing, which is how the callers tell a tag from any other macro; no tag falls through to another tag.
Algorithm
- Read
namefromx. AGlobalRefgives itsnamefield, aSymbolgives itself, and any other value returnsnothingat once. - Walk
PROP_TAG_NAMESandPROP_TAG_MACRO_NAMEStogether. Return the tag whose macro name is identical toname. - No macro name matches: return
nothing.
The comparison is === on a Symbol, so a macro whose name merely resembles a tag never matches.
Arguments
x: The first argument of a:macrocallexpression.
Returns
tag::Symbol: The tag name, without the@.nothing: Ifxnames no tag.
Related
PortfolioOptimisers.is_prop_tag_call — Function
is_prop_tag_call(x) -> Bool
Return true if x is a macro call to any tag of PROP_TAG_NAMES.
Used by peel_prop_tags and propagatable_parse_body to detect tagged fields in a struct body.
Algorithm
- Return
truewhen all three hold:xis anExpr, its head is:macrocall, andprop_tagof its first argument is notnothing. - Return
falseotherwise.
Arguments
x: Any expression appearing in a struct body.
Related
PortfolioOptimisers.prop_tag_expr — Function
prop_tag_expr(
channel::Symbol,
tag::Symbol,
fname::Symbol,
xf,
mod::Module,
thread
) -> Expr
Return the expression that a tag substitutes for one field, inside a generated method.
This is the name → field transform half of the tag table: one branch per tag of PROP_TAG_NAMES, read by prop_channel_pairs for every channel. A tag of the table with no branch here errors, so it cannot silently take another tag's transform.
Algorithm
The channel is read first, and then the tag, because one tag has two transforms.
channelis:obs:tagis:wprop: returnnothing_scalar_array_getindex(xf, thread...). The field is the weights, so it is indexed to the selected observations. Indexing keeps theAbstractWeightssubtype, which a view would not.tagis:fprop: returnobs_weights_view(xf, thread...). The field is a composed child, so the verb recurses into it.
channelis any other channel:tagis:fprop: returnfactory_child(xf, thread..., args...; kwargs...).tagis:vprop: returnview_child(xf, thread..., args...), which callsport_opt_viewon every value but a precomputed optimisation result. This channel forwards no keywords.tagis:pprop: returnsel(xf, getproperty(pr, fname)), which is why the field name is an argument. The prior result supplies the property of the same name.tagis:cprop: returnsel(xf, _ctx(args...)), which reads the context out of the threaded arguments rather than the prior.tagis:wprop: return_wprop(xf, args...; kwargs...), which replaces the field with an incomingObsWeights.
- No branch matched: raise an error naming the tag and the channel, and ask for a branch here.
Steps 1.1 and 2.5 are the same tag with two transforms, so the channel decides what @wprop means. Every emitted name is qualified against mod, because the expansion is escaped into the caller's module.
Arguments
tag::Symbol: A tag ofPROP_TAG_NAMES.fname::Symbol: The field name, needed by@ppropto name the prior property.xf: The expression that reads the field off the incoming struct.mod::Module: The module that defines@propagatable. Every emitted name is qualified against it, because the expansion is escaped into the caller.thread: Extra positional arguments the channel threads beforeargs....
Returns
expr::Expr: The value of the field in the generated constructor call.
Related
PortfolioOptimisers.prop_channel_active — Function
prop_channel_active(
channel::Symbol,
tagged::AbstractDict
) -> Any
Return true if a channel of PROP_TAG_CHANNELS must emit a method.
A channel is active when at least one field carries a tag of the channel's gate.
Algorithm
- Read the
gatetuple of the channel fromPROP_TAG_CHANNELS. - Return
truewhen at least one tag ofgatehas a non-empty entry intagged, andfalseotherwise.
The precedence tuple is not read here, so a tag that a channel consults but does not gate on never makes that channel emit a method on its own. The obs channel gates on @wprop alone and consults @fprop, so a type carrying @fprop and no @wprop gains no obs method.
Arguments
channel::Symbol: A channel name ofPROP_TAG_CHANNELS.tagged::AbstractDict: Tag name to the field names that carry it, frompropagatable_parse_body.
Related
PortfolioOptimisers.prop_channel_pairs — Function
prop_channel_pairs(
channel::Symbol,
tagged::AbstractDict,
all_fields::AbstractVector{Symbol},
obj::Symbol,
mod::Module,
thread
) -> Vector{Any}
Return the keyword pairs of the constructor call that one channel generates.
Every declared field gets one pair, in declaration order. The field's tags are consulted in the channel's precedence order; the first match gives the value through prop_tag_expr, and a field carrying no tag of the channel is passed through unchanged.
Algorithm
- Read the
precedencetuple of the channel fromPROP_TAG_CHANNELS. - For each field name
fnameofall_fields, in declaration order:- Build
xf, the expressionobj.fnamethat reads the field off the incoming struct. - Find
idx, the position of the first tag ofprecedencethatfnamecarries. - When
idxisnothing, the field carries no tag of this channel: the value isxfitself. - Otherwise the value is
prop_tag_exprof that tag, in this channel. - Push
Expr(:kw, fname, value)ontopairs.
- Build
- Return
pairs.
Step 2.2 is where the precedence decides one field's transform. A field carrying @pprop and @fprop takes the @pprop transform on the prior channel and the @fprop transform on the factory channel, because the two channels order the tags differently.
Arguments
channel::Symbol: A channel name ofPROP_TAG_CHANNELS.tagged::AbstractDict: Tag name to the field names that carry it.all_fields::AbstractVector{Symbol}: Every declared field, in declaration order.obj::Symbol: The struct the generated method reads the fields off.mod::Module: The module that defines@propagatable.thread: Extra positional arguments the channel threads beforeargs....
Returns
pairs::Vector{Any}: OneExpr(:kw, field, value)per declared field.
Related
PortfolioOptimisers.check_prop_tag_macros — Function
check_prop_tag_macros()
check_prop_tag_macros(tags)
check_prop_tag_macros(tags, macro_names)
check_prop_tag_macros(tags, macro_names, channels)
check_prop_tag_macros(
tags,
macro_names,
channels,
mod::Module
)
Check that every tag of PROP_TAG_NAMES is complete.
A tag row is complete when it has a stub macro, a channel of PROP_TAG_CHANNELS that names it, and a field transform in prop_tag_expr for every channel that names it. A row that lacks one of the three is a tag that parses but never propagates, which is the failure the table exists to stop. The per-channel probe is what catches a tag added to a second channel without a transform there, now that a tag means what its channel says it means. All the violations are collected and reported together.
Runs once at the end of the module. Throws an ArgumentError listing every violation, so the package refuses to precompile rather than shipping a dead tag.
The three tables are arguments, and each one defaults to the table the module ships. The shipped tables are complete, so the call the module makes never reports a violation; a caller that passes a table of its own drives each of the three clauses and reads the message it gives.
Algorithm
- Make
violations, an empty vector of strings. - For each tag of
PROP_TAG_NAMES, with its macro name fromPROP_TAG_MACRO_NAMES:- The macro name is not defined in this module: push a message that the tag declares no stub macro.
- The tag appears in the
precedenceof no channel ofPROP_TAG_CHANNELS: push a message that the tag appears in no channel. - For each channel whose
precedencenames the tag, callprop_tag_exprwith the probe name:probe. When that call raises, push a message naming the tag and the channel.
violationsis not empty: throw anArgumentErrorlisting every one of them.- Return
nothing.
Step 2.3 probes each channel that names the tag, not the tag alone. This is what catches a tag added to a second channel with no transform there, which the whole-tag probe of an earlier design let through.
Arguments
tags: The tag names to check.macro_names: The stub macro name of each tag, in the order oftags.channels: The channel table whoseprecedencetuples are read.mod::Module: The module the stub macros are looked up in, and the module that qualifies the namesprop_tag_expremits.
Returns
nothing: Every row oftagsis complete.
Related
PortfolioOptimisers.is_doc_macro — Function
is_doc_macro(x) -> Union{Missing, Bool}
Return true if x is a reference to Julia's @doc macro (bare Symbol or GlobalRef).
Used by propagatable_parse_body to recognise docstring-prefixed fields in a struct body.
Algorithm
- Return
truewhenxis aGlobalRefwhosenameisSymbol("@doc"). - Return
truewhenxis equal toSymbol("@doc"). - Return
falseotherwise.
Both spellings are needed for the same reason prop_tag needs both: a struct body written by hand carries the bare Symbol, and one that another macro has already expanded carries the GlobalRef.
Related
PortfolioOptimisers._ctx — Function
_ctx(args...)Locate the lone threaded optimiser context value (a solver, Slv_VecSlv) in the variadic tail of a prior factory call, returning nothing if none is present. Emitted by the @cprop tag as the source argument to sel. The tuple scan is unrolled by the compiler, so it is type-stable and allocation-free.
Related
PortfolioOptimisers.extract_field_name — Function
extract_field_name(expr) -> Any
Extract the field name Symbol from a bare field or field::Type expression.
Errors with a descriptive message when expr is neither a bare Symbol nor a field::Type annotation, since only those forms are valid after @fprop.
Algorithm
expris aSymbol: return it.expris anExprwhose head is:(::): return its first argument, which is the field name.expris anything else: raise an error naming the expression.
Step 3 is what separates this function from try_field_name, which returns nothing in the same case. A tag states that the node is a field, so a node that is not one is a defect in the struct body and not a node to skip.
Arguments
expr: ASymbol, anExprwith head:(::), or any other expression (triggers an error).
Returns
name::Symbol: The field name.
Related
PortfolioOptimisers.propagatable_find_struct — Function
propagatable_find_struct(
expr
) -> Union{Tuple{Expr, typeof(identity)}, Tuple{Expr, Union{PortfolioOptimisers.var"#propagatable_find_struct##0#propagatable_find_struct##1"{Vector{Any}, typeof(identity)}, PortfolioOptimisers.var"#propagatable_find_struct##0#propagatable_find_struct##1"{Vector{Any}, PortfolioOptimisers.var"#propagatable_find_struct##0#propagatable_find_struct##1"{Vector{Any}, rebuild}} where rebuild}}}
Recursively unwrap macro call chains to locate the innermost :struct node.
Returns (struct_node, rebuild_fn) where rebuild_fn(new_struct) reconstructs the original macro chain with new_struct in place of the original struct. This allows @propagatable to inject modified struct definitions back into arbitrary macro wrappers such as @concrete.
Algorithm
expris not anExpr: raise an error naming its type.exprhas head:struct: returnexpritself andidentity, which rebuilds nothing.exprhas head:macrocall: takeinner, its last argument, and call this function again on it. That call givesstruct_nodeandrebuild. Readprefix, every argument ofexprexcept the last. Returnstruct_nodeand the functions -> Expr(:macrocall, prefix..., rebuild(s)).exprhas any other head: raise an error naming the head.
Step 3 rebuilds the chain from the inside out, so a struct wrapped in several macros comes back wrapped in the same macros, in the same order, with the same arguments. The prefix carries the macro's own arguments and its LineNumberNode, so nothing of the call is lost.
Arguments
expr: A:structexpression or a:macrocallexpression wrapping one.
Returns
struct_node::Expr: The innermost:structexpression.rebuild_fn::Function: A function that, given a replacement:struct, returns the full macro chain with the replacement in place of the original.
Related
PortfolioOptimisers.propagatable_bare_name — Function
propagatable_bare_name(n) -> Symbol
Extract the plain struct name Symbol from a potentially parameterised or supertype-constrained name expression.
Handles the forms Name, Name{T, ...}, and Name{T, ...} <: SuperType by recursively peeling :curly and :<: wrappers until a bare Symbol is reached.
Algorithm
nis aSymbol: return it.nhas head:curly: call this function again on its first argument, which drops the type parameters.nhas head:<:: call this function again on its first argument, which drops the supertype.nis anything else: raise an error naming the expression.
Steps 2 and 3 compose, so Name{T} <: Super peels the supertype first and then the parameters.
Arguments
n: ASymbol, or anExprwith head:curlyor:<:.
Returns
name::Symbol: The plain struct name.
Related
PortfolioOptimisers.try_field_name — Function
try_field_name(expr) -> Any
Return the field name Symbol for a plain field declaration, or nothing for non-field nodes.
Recognises bare Symbol fields and field::Type annotations. Returns nothing for LineNumberNodes, inner constructors, and any other expression that does not declare a single named field.
Algorithm
expris aSymbol: return it.exprhas head:(::)and its first argument is aSymbol: return that argument.expris anything else: returnnothing.
Step 2 tests the first argument as well as the head, which extract_field_name does not. A node such as ::Type, which annotates no name, therefore gives nothing here and reaches step 3.
Arguments
expr: Any expression appearing in a struct body.
Returns
name::Symbol: The field name, ifexpris a plain field declaration.nothing: Ifexpris not a plain field declaration.
Related
PortfolioOptimisers.peel_prop_tags — Function
peel_prop_tags(expr) -> Tuple{Set{Symbol}, Any}
Peel any stack of tag macrocalls off a field expression, recording which tags were present.
Tags may be stacked in either order (@pprop @fprop field), which parses as nested :macrocall nodes; this unwraps them all and returns the bare field expression. Each tag is looked up with prop_tag, so an untagged macro stops the peel and no tag is reached by falling through the others.
Algorithm
- Make
tags, an emptySet{Symbol}. - While
is_prop_tag_callofexprholds, pushprop_tagof its first argument ontotags, and replaceexprwith its last argument, which is the expression the tag wraps. - Return
tagsand the peeledexpr.
tags is a set, so a tag written twice on one field is recorded once. The loop stops at the first node that is not a tag call, so a non-tag macro between two tags hides the tags below it.
Arguments
expr: A field expression, with or without tag macrocalls around it.
Returns
tags::Set{Symbol}: The tags ofPROP_TAG_NAMESthatexprcarries.stripped: The field expression with all tags removed.
Related
PortfolioOptimisers.propagatable_parse_body — Function
propagatable_parse_body(
body
) -> Tuple{Dict{Symbol, Vector{Symbol}}, Vector{Symbol}, Expr}
Walk a struct body, collecting the tagged field names (and all field names) and stripping the tags from the body.
Handles bare tagged fields (@fprop field, …), stacked tags (@pprop @fprop field, in any order), and docstring-prefixed forms ("doc" \n @fprop field). Non-field nodes (line numbers, inner constructors) are carried through unchanged. The tags are the rows of PROP_TAG_NAMES, so a new tag needs no change here.
Algorithm
- Make
tagged, one empty vector per tag ofPROP_TAG_NAMES;all_fields, an empty vector; andnew_args, an empty vector for the stripped body. - For each node
argof the struct body, in declaration order, take one of three branches:argis a@docmacrocall, which is how a documented field parses. Peel the tags offinner, its last argument.- The field carries at least one tag: record the field name under each of its tags and in
all_fields, then push a rebuilt@docnode whose last argument is the stripped field. - The field carries no tag: record its name in
all_fieldswhentry_field_namefinds one, and pushargunchanged.
- The field carries at least one tag: record the field name under each of its tags and in
argis a tag macrocall with no docstring: peel the tags, record the field name under each of them and inall_fields, and push the stripped field expression.argis anything else — aLineNumberNode, an untagged field, an inner constructor: record its name inall_fieldswhentry_field_namefinds one, and pushargunchanged.
- Return
tagged,all_fields, and the new body as one:blockexpression.
all_fields holds every declared field, tagged or not, and it is what makes the generated constructor call name every keyword. The returned body carries no tag, so the wrapped macros and Julia itself never see one.
Arguments
body::Expr: The:blockexpression forming the struct body.
Returns
tagged::Dict{Symbol, Vector{Symbol}}: One entry per tag ofPROP_TAG_NAMES, holding the names of the fields that carry it, in declaration order.all_fields::Vector{Symbol}: Names of every declared field (tagged or not).new_body::Expr: The struct body with all tags stripped.
Related
PortfolioOptimisers.propagatable_register! — Function
propagatable_register!(
T::Type,
pprops::Tuple{Vararg{Symbol}}
)
Record type T and its @pprop-tagged field names in PROPAGATABLE_CONTRACTS.
Called by @propagatable at the declaration itself. It only records: the outer keyword constructor is written below the struct, so it does not exist yet and cannot be checked here. check_propagatable_contracts does the checking once the module is complete.
Algorithm
- Push the pair
(T, pprops)ontoPROPAGATABLE_CONTRACTS. - Return
nothing.
T is @nospecialized, so one method serves every registered type and the registration costs no compilation.
Related
PortfolioOptimisers.propagatable_keywords — Function
propagatable_keywords(T::Type) -> Vector{Symbol}
Return the keyword names accepted by the outer constructors of T, unioned over its methods.
A kwargs... slurp is dropped rather than counted. A slurp accepts field = value and then discards it, which is the silent failure this check exists to catch, so it must not satisfy the contract.
Algorithm
- Make
kws, an empty vector of symbols. - For each method
mof the constructorT, appendBase.kwarg_decl(m)tokws. The union runs over every outer constructor, so a keyword that any one of them names counts. - Remove the repeats from
kws. - Remove every name whose string ends in
..., which is howBase.kwarg_declreports a slurp. - Return
kws.
Step 4 is the whole point of the function. A constructor that carries kwargs... reports the slurp as a keyword name, and counting it would let every field satisfy the contract.
Related
PortfolioOptimisers.propagatable_contract_violations — Function
propagatable_contract_violations(
T::Type,
pprops,
pool
) -> Vector{String}
Return the broken clauses of one type's @propagatable contract, as messages.
Two clauses are checked, and both are properties of the code the macro emits:
- Every field name is a keyword of the outer constructor. Each generated method rebuilds the struct with
StructName(; field = …)over all fields, tagged or not, so one field that the keyword constructor does not name is aMethodErrorat the firstfactoryorport_opt_viewcall. - Every
@ppropfield is a property of a prior result. The generatedfactory(x, pr::AbstractPriorResult, args...)readsgetproperty(pr, :field), so a name absent fromprior_result_property_poolthrows when a prior is threaded.
The messages carry a suggest_declared_key suggestion, so a transposed or mistyped field name names its intended neighbour.
Algorithm
- Make
msgs, an empty vector of strings. - Read
kws, the keywords of the outer constructors ofT, withpropagatable_keywords. - For each field name of
Tthat is absent fromkws, push a message naming the type, the field and thesuggest_declared_keysuggestion drawn fromkws. - For each name of
ppropsthat is absent frompool, push a message naming the type, the field and the suggestion drawn frompool. - Return
msgs.
Every clause is collected, and none stops the walk, so one call reports every violation of one type at once. An empty result means that the type's contract holds.
Related
PortfolioOptimisers.check_propagatable_contracts — Function
check_propagatable_contracts()
check_propagatable_contracts(contracts)
check_propagatable_contracts(contracts, pool)
Check the @propagatable contract of every type in PROPAGATABLE_CONTRACTS.
Runs once at the end of the module, so the contract behind every generated method is enforced where the structs are declared rather than at the first factory call. The violations of every type are collected and reported together, because a run that stops at the first one hides the rest. See propagatable_contract_violations for the two clauses.
Throws an ArgumentError listing every violation; the package refuses to precompile rather than shipping a type whose generated methods throw on first use. A package that declares its own @propagatable types calls this at the end of its own module to get the same guarantee.
Algorithm
- Read
pool, the property names that a prior result can carry, withprior_result_property_pool. - Make
msgs, an empty vector of strings. - For each pair
(T, pprops)ofPROPAGATABLE_CONTRACTS, append the messages thatpropagatable_contract_violationsreports for that type. msgsis not empty: throw anArgumentErrornaming the count and listing every message.- Return
nothing.
Step 3 collects and never stops, so one run reports the violations of every registered type. The registry is filled by propagatable_register! at each declaration, so this function must run after the last one, which is why the module calls it at its end.
Both the registry and the pool are arguments, and each defaults to the value the module ships. A caller that passes a registry of its own reads the message a broken contract gives, without registering a broken type.
Arguments
contracts: The pairs of a type and its@pprop-tagged field names to check.pool: The property names a prior result can carry.
Returns
nothing: Every pair ofcontractssatisfies the contract.
Related