Propagatable: private API

Utility functions

PortfolioOptimisers.PROP_TAG_NAMES — Constant
PROP_TAG_NAMES

The 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

source
PortfolioOptimisers.PROP_TAG_MACRO_NAMES — Constant
PROP_TAG_MACRO_NAMES

The 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

  1. Map each tag of PROP_TAG_NAMES to Symbol("@", 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

source
PortfolioOptimisers.PROP_TAG_CHANNELS — Constant
PROP_TAG_CHANNELS

The 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 @propagatable emit 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

source
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

  1. Read name from x. A GlobalRef gives its name field, a Symbol gives itself, and any other value returns nothing at once.
  2. Walk PROP_TAG_NAMES and PROP_TAG_MACRO_NAMES together. Return the tag whose macro name is identical to name.
  3. 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 :macrocall expression.

Returns

  • tag::Symbol: The tag name, without the @.
  • nothing: If x names no tag.

Related

source
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

  1. Return true when all three hold: x is an Expr, its head is :macrocall, and prop_tag of its first argument is not nothing.
  2. Return false otherwise.

Arguments

  • x: Any expression appearing in a struct body.

Related

source
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.

  1. channel is :obs:
    1. tag is :wprop: return nothing_scalar_array_getindex(xf, thread...). The field is the weights, so it is indexed to the selected observations. Indexing keeps the AbstractWeights subtype, which a view would not.
    2. tag is :fprop: return obs_weights_view(xf, thread...). The field is a composed child, so the verb recurses into it.
  2. channel is any other channel:
    1. tag is :fprop: return factory_child(xf, thread..., args...; kwargs...).
    2. tag is :vprop: return view_child(xf, thread..., args...), which calls port_opt_view on every value but a precomputed optimisation result. This channel forwards no keywords.
    3. tag is :pprop: return sel(xf, getproperty(pr, fname)), which is why the field name is an argument. The prior result supplies the property of the same name.
    4. tag is :cprop: return sel(xf, _ctx(args...)), which reads the context out of the threaded arguments rather than the prior.
    5. tag is :wprop: return _wprop(xf, args...; kwargs...), which replaces the field with an incoming ObsWeights.
  3. 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 of PROP_TAG_NAMES.
  • fname::Symbol: The field name, needed by @pprop to 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 before args....

Returns

  • expr::Expr: The value of the field in the generated constructor call.

Related

source
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

  1. Read the gate tuple of the channel from PROP_TAG_CHANNELS.
  2. Return true when at least one tag of gate has a non-empty entry in tagged, and false otherwise.

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

Related

source
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

  1. Read the precedence tuple of the channel from PROP_TAG_CHANNELS.
  2. For each field name fname of all_fields, in declaration order:
    1. Build xf, the expression obj.fname that reads the field off the incoming struct.
    2. Find idx, the position of the first tag of precedence that fname carries.
    3. When idx is nothing, the field carries no tag of this channel: the value is xf itself.
    4. Otherwise the value is prop_tag_expr of that tag, in this channel.
    5. Push Expr(:kw, fname, value) onto pairs.
  3. 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 of PROP_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 before args....

Returns

  • pairs::Vector{Any}: One Expr(:kw, field, value) per declared field.

Related

source
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

  1. Make violations, an empty vector of strings.
  2. For each tag of PROP_TAG_NAMES, with its macro name from PROP_TAG_MACRO_NAMES:
    1. The macro name is not defined in this module: push a message that the tag declares no stub macro.
    2. The tag appears in the precedence of no channel of PROP_TAG_CHANNELS: push a message that the tag appears in no channel.
    3. For each channel whose precedence names the tag, call prop_tag_expr with the probe name :probe. When that call raises, push a message naming the tag and the channel.
  3. violations is not empty: throw an ArgumentError listing every one of them.
  4. 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 of tags.
  • channels: The channel table whose precedence tuples are read.
  • mod::Module: The module the stub macros are looked up in, and the module that qualifies the names prop_tag_expr emits.

Returns

  • nothing: Every row of tags is complete.

Related

source
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

  1. Return true when x is a GlobalRef whose name is Symbol("@doc").
  2. Return true when x is equal to Symbol("@doc").
  3. Return false otherwise.

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

source
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

source
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

  1. expr is a Symbol: return it.
  2. expr is an Expr whose head is :(::): return its first argument, which is the field name.
  3. expr is 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: A Symbol, an Expr with head :(::), or any other expression (triggers an error).

Returns

  • name::Symbol: The field name.

Related

source
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

  1. expr is not an Expr: raise an error naming its type.
  2. expr has head :struct: return expr itself and identity, which rebuilds nothing.
  3. expr has head :macrocall: take inner, its last argument, and call this function again on it. That call gives struct_node and rebuild. Read prefix, every argument of expr except the last. Return struct_node and the function s -> Expr(:macrocall, prefix..., rebuild(s)).
  4. expr has 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 :struct expression or a :macrocall expression wrapping one.

Returns

  • struct_node::Expr: The innermost :struct expression.
  • rebuild_fn::Function: A function that, given a replacement :struct, returns the full macro chain with the replacement in place of the original.

Related

source
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

  1. n is a Symbol: return it.
  2. n has head :curly: call this function again on its first argument, which drops the type parameters.
  3. n has head :<:: call this function again on its first argument, which drops the supertype.
  4. n is 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: A Symbol, or an Expr with head :curly or :<:.

Returns

  • name::Symbol: The plain struct name.

Related

source
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

  1. expr is a Symbol: return it.
  2. expr has head :(::) and its first argument is a Symbol: return that argument.
  3. expr is anything else: return nothing.

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, if expr is a plain field declaration.
  • nothing: If expr is not a plain field declaration.

Related

source
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

  1. Make tags, an empty Set{Symbol}.
  2. While is_prop_tag_call of expr holds, push prop_tag of its first argument onto tags, and replace expr with its last argument, which is the expression the tag wraps.
  3. Return tags and the peeled expr.

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 of PROP_TAG_NAMES that expr carries.
  • stripped: The field expression with all tags removed.

Related

source
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

  1. Make tagged, one empty vector per tag of PROP_TAG_NAMES; all_fields, an empty vector; and new_args, an empty vector for the stripped body.
  2. For each node arg of the struct body, in declaration order, take one of three branches:
    1. arg is a @doc macrocall, which is how a documented field parses. Peel the tags off inner, its last argument.
      1. The field carries at least one tag: record the field name under each of its tags and in all_fields, then push a rebuilt @doc node whose last argument is the stripped field.
      2. The field carries no tag: record its name in all_fields when try_field_name finds one, and push arg unchanged.
    2. arg is a tag macrocall with no docstring: peel the tags, record the field name under each of them and in all_fields, and push the stripped field expression.
    3. arg is anything else — a LineNumberNode, an untagged field, an inner constructor: record its name in all_fields when try_field_name finds one, and push arg unchanged.
  3. Return tagged, all_fields, and the new body as one :block expression.

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 :block expression forming the struct body.

Returns

  • tagged::Dict{Symbol, Vector{Symbol}}: One entry per tag of PROP_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

source
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

  1. Push the pair (T, pprops) onto PROPAGATABLE_CONTRACTS.
  2. Return nothing.

T is @nospecialized, so one method serves every registered type and the registration costs no compilation.

Related

source
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

  1. Make kws, an empty vector of symbols.
  2. For each method m of the constructor T, append Base.kwarg_decl(m) to kws. The union runs over every outer constructor, so a keyword that any one of them names counts.
  3. Remove the repeats from kws.
  4. Remove every name whose string ends in ..., which is how Base.kwarg_decl reports a slurp.
  5. 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

source
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 a MethodError at the first factory or port_opt_view call.
  • Every @pprop field is a property of a prior result. The generated factory(x, pr::AbstractPriorResult, args...) reads getproperty(pr, :field), so a name absent from prior_result_property_pool throws 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

  1. Make msgs, an empty vector of strings.
  2. Read kws, the keywords of the outer constructors of T, with propagatable_keywords.
  3. For each field name of T that is absent from kws, push a message naming the type, the field and the suggest_declared_key suggestion drawn from kws.
  4. For each name of pprops that is absent from pool, push a message naming the type, the field and the suggestion drawn from pool.
  5. 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

source
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

  1. Read pool, the property names that a prior result can carry, with prior_result_property_pool.
  2. Make msgs, an empty vector of strings.
  3. For each pair (T, pprops) of PROPAGATABLE_CONTRACTS, append the messages that propagatable_contract_violations reports for that type.
  4. msgs is not empty: throw an ArgumentError naming the count and listing every message.
  5. 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 of contracts satisfies the contract.

Related

source