|
| 1 | +# Attributes v2 and Hooks |
| 2 | + |
| 3 | +## Preamble |
| 4 | + |
| 5 | + Author: Paul Evans <PEVANS> |
| 6 | + Sponsor: |
| 7 | + ID: TODO |
| 8 | + Status: Exploratory |
| 9 | + |
| 10 | +## Abstract |
| 11 | + |
| 12 | +Jointly: |
| 13 | + |
| 14 | +* define a new way that attribute definitions can be introduced such that the parser can invoke the third-party custom logic they provide; and additionally |
| 15 | + |
| 16 | +* define more extensive kinds of magic-like structure for attaching custom behaviour onto existing Perl data structures, such as variables, subroutines, and parts thereof. |
| 17 | + |
| 18 | +These two ideas are presented together in one document because, while there could be some valid use cases of each on its own, it is the combination of the two that provides most of the power to create extension modules that can extend the language in new ways. |
| 19 | + |
| 20 | +Throughout this document, the term "third-party" means any behaviour provided by additional modules loaded into the interpreter; whether these modules are shipped with the core perl distribution, on CPAN, or privately implemented by other means. This is distinct from true "builtin" behaviours, which are provided by the interpreter itself natively. |
| 21 | + |
| 22 | +## Motivation |
| 23 | + |
| 24 | +### Attributes |
| 25 | + |
| 26 | +The Perl parser allows certain syntax elements, namely the declaration of variables and subroutines, to be annotated with additional information, called "attributes". Each attribute consists of a name and optionally a plain string argument, supplied after a leading colon after the declaration of the name of the element it is attached to. |
| 27 | + |
| 28 | +There are a few of these attribute definitions built into core perl itself; for example: |
| 29 | + |
| 30 | +```perl |
| 31 | +my $counter :shared; |
| 32 | + |
| 33 | +sub name :lvalue { ... } |
| 34 | +``` |
| 35 | + |
| 36 | +The parser supports additional attributes that can be provided by modules, though the situation here has many shortcomings: |
| 37 | + |
| 38 | +* Only lexical or package variables, and subroutines, support attribute syntax. It is not possible to declare attributes on other entities such as packages, or subroutine parameters. |
| 39 | + |
| 40 | +* Attributes on anonymous subroutines are invoked only at "clonecv"-time; the time when an anonymous function gets turned into a closure with variable captures. It cannot perform any behaviour before this time. |
| 41 | + |
| 42 | +* Attribute handling is looked up via the package heirarchy expressed in `@ISA`, rather than by lexical scope. |
| 43 | + |
| 44 | +* Third-party attributes are implemented by providing a single `MODIFY_*_ATTRIBUTES` shouty-named method in a package, which is required to understand all the attributes at once. There is no standard mechanism to create individual attributes independently. |
| 45 | + |
| 46 | +Many of these restrictions come from the way that third-party attributes are provided in the perl core, quite apart from its own built-in handling. Built-in attributes get to run their logic much earlier in the parser. |
| 47 | + |
| 48 | +It is the aim of this specification to provide a better and more flexible way for third-party modules to declare and use attributes to allow authors to declare more interesting behaviours on elements of their code. |
| 49 | + |
| 50 | +### Hooks |
| 51 | + |
| 52 | +In addition to the limitations of attribute syntax described above, there are very limited options available to the would-be implementors of attributes, as ways to provide the behaviour their attribute would have. |
| 53 | + |
| 54 | +Scalar variables in Perl support a concept called "magic", by which custom behaviour can be attached onto a variable, to be invoked whenever the variable is read from, written to, or goes out of scope. While in theory other kinds of variables (arrays and hashes) do support magic, in practice the magic is not truely aware of the container-like nature of the variable to which they are attached, and cannot provide customisation around things like element iteration or access, without a lot of weird tricks. Magic on any other kind of SV (such as subroutines or stashes, the data stores used to implement packages and classes) is virtually non-existent, limited only to passive storage of additional notation data, and notification of the attached entity's destruction. Other concepts that are not even backed by true SVs, such as the abstract notion of a subroutine parameter or an object field, do not support magic at all. |
| 55 | + |
| 56 | +Many limitations of the core magic system can be seen in core perl itself. While magic is used to implement a lot of the interesting scalar variables (such as `$$` for fetching the current process's PID, or `$&`, the result of the most recent regexp match), it is not used directly for creating things like array or hash variables with custom behaviour, or providing tie on these things. While there _is_ magic involved in these concepts, that magic is often just used as a marker to store extra information to allow special-purpose code in the interpreter to implement those other behaviours. Yet other kinds of magic are used for more tagging of additional data that don't provide additional behaviour on their own. |
| 57 | + |
| 58 | +It would seem that the existing concept of "magic" in the Perl core is simultanously too limited and inflexible to provide most interesting custom behaviours, and at the same time overly elaborate for simply attaching additional data or destruction notification onto non-scalar variables. |
| 59 | + |
| 60 | +It is the aim of this specification to provide a new generation of magic-like behaviour, both for core perl to use for its own purposes in a far more uniform way, and to allow third-party module authors to attach more interesting behaviours when requested; perhaps by using an attribute. |
| 61 | + |
| 62 | +## Rationale |
| 63 | + |
| 64 | +(explain why the (following) proposed solution will solve it) |
| 65 | + |
| 66 | +## Specification |
| 67 | + |
| 68 | +### Attributes |
| 69 | + |
| 70 | +Attributes defined by this specification will be lexical in scope, much like that of a `my` variable, `my sub` function, or any of the builtin function exports provided by the `builtin` module. This provides a clean separation of naming within the code. |
| 71 | + |
| 72 | +An attribute is defined in its base layer, by a single C callback function, to be invoked by the parser _as soon as_ it has finished parsing the declaration syntax. This callback function will be passed the target (i.e. the item to which the attribute is being attached), and the optional contents of the parentheses used as an argument to the attribute. There is no interesting return value from this callback function. |
| 73 | + |
| 74 | +```c |
| 75 | +typedef void PerlAttributeCallback(pTHX_ SV *target, SV *attrvalue); |
| 76 | +``` |
| 77 | +
|
| 78 | +In order to create a new attribute, a third-party module author would create such a C function containing whatever behaviour is required, and wrap it in some kind of SV - whose type is still yet to be determined (see "Open Issues" below). This wrapping SV is then placed into the importing scope's lexical pad, using a `:` sigil and the name the attribute should use. It is important to stress that this SV represents the abstract concept of the attribute _in general_, rather than its application to any particular target. As the definition of an attribute itself is not modified or consumed by any particular application of it, a single SV to represent it can be shared and reused by any module that imports it. |
| 79 | +
|
| 80 | +When the parser is parsing perl code and finds an attribute declaration attached to some entity, it can immediately inspect the lexical pad (and recurse up to parent scopes if applicable) in an attempt to find one of these lexical definitions. The first one that is found is invoked immediately, before the parser moves on in the source code. If such an attempt does not find a suitable handler, the declaration can be stored using the existing mechanism for a later attempt via the previous implementation. |
| 81 | +
|
| 82 | +When attached to a package variable or package-named function, the target can be the entity itself (or maybe indirectly, its GV). When attached to a lexical, it is important to pass in the abstract concept of the target in general, rather than the current item in its scope pad. There is currently no suitable kind of SV to represent this - this remains another interesting open issue. |
| 83 | +
|
| 84 | +Additionally, new callsites can be added to the parser to invoke attribute callbacks in new situations that previously were not permitted; such as package declarations or subroutine parameters. |
| 85 | +
|
| 86 | +Note that this specification does not provide a mechanism by which attributes can declare what kinds of targets they are applicable to. Any particular named attribute will be attempted for _any_ kind of target that the parser finds. It is the job of the attribute callback itself to enquire what kind of target it has been invoked on, and reject it if necessary - likely with a `croak()` call of some appropriate message. |
| 87 | +
|
| 88 | +```c |
| 89 | +void attribute_callback_CallMeOnce(pTHX_ SV *target, SV *attrvalue) |
| 90 | +{ |
| 91 | + if(SvTYPE(target) != SVt_PVCV) |
| 92 | + croak("Can only apply :CallMeOnce to a subroutine"); |
| 93 | +
|
| 94 | + ... |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +As there is no interesting result returned from the attribute callback function, it must perform whatever work it needs to implement the requested behaviour purely as a side-effect of running it. While a few built-in attributes can be implemented perhaps by adjusting SV flags (such as `:lvalue` simply calling `CvLVALUE_on(cv)`), the majority of interesting use-cases would need to apply some form of extension hook to the target entity. These hooks are described in the other half of this proposal. |
| 99 | + |
| 100 | +### Hooks |
| 101 | + |
| 102 | +(details on how the thing is intended to work) |
| 103 | + |
| 104 | +## Backwards Compatibility |
| 105 | + |
| 106 | +### Attributes |
| 107 | + |
| 108 | +The new mechanism proposed here is entirely lexically scoped. Any attributes introduced into a scope will not be visible from outside. As such, it is an entirely opt-in effect that would not cause issues for existing code that is not expecting it. |
| 109 | + |
| 110 | +### Hooks |
| 111 | + |
| 112 | +## Security Implications |
| 113 | + |
| 114 | +## Examples |
| 115 | + |
| 116 | +As both parts of this proposal are interpreter internal components that are intended for XS authors to use to provide end-user features, it would perhaps be more useful to consider examples of the kinds of modules that the combination of these two features would permit to be created. |
| 117 | + |
| 118 | +For example, a subroutine attribute `:void` could be created that forces the caller context of any `return` ops within the body of the subroutine, or its implicit end-of-scope expressions, to make them always run in void context. |
| 119 | + |
| 120 | +```perl |
| 121 | +use Subroutine::Attribute::Void; |
| 122 | + |
| 123 | +sub debug($msg) :void { |
| 124 | + print STDERR "DEBUG:> $msg\n"; |
| 125 | +} |
| 126 | + |
| 127 | +print debug("start"), "middle", debug("end"); |
| 128 | +# Output printed to STDOUT is simply "middle", without the leading or trailing |
| 129 | +# "1" return value from the print statement inside the function. |
| 130 | +``` |
| 131 | + |
| 132 | +This kind of attribute would be easy to implement with some optree adjustment in the hook applied by the attribute, but would not be possible to create by the existing mechanisms because they cannot run at the right times. |
| 133 | + |
| 134 | +## Prototype Implementation |
| 135 | + |
| 136 | +As both mechanisms proposed by this document would need to be implemented by perl core itself, it is difficult to provide a decent prototype for as an experimental basis. |
| 137 | + |
| 138 | +However, both parts of the mechanism are similar to existing technology currently used in [`Object::Pad`](https://metacpan.org/pod/Object::Pad) for providing third-party extension attributes. In `Object::Pad` the two mechanisms are conflated together - extension hooks can be provided, but must be registered with an attribute name. Source code that requests that attribute then gets that set of hooks attached. |
| 139 | + |
| 140 | +The mechanism proposed here makes the following improvements over the existing approach in `Object::Pad`: |
| 141 | + |
| 142 | +* The concept of named attributes and extension hooks are separated. While each is intended to be used largely by the other, they are independent in case situations require the use of each separately. |
| 143 | + |
| 144 | +* By storing the attribute definitions in the pad, each local scope can give the attribute its own name, allowing renaming in scopes if that would avoid name collisions. In comparison, the ones in `Object::Pad` have a single fixed name, whose visiblity is simply enabled by a lexically-scoped key in the hint hash. |
| 145 | + |
| 146 | +## Future Scope |
| 147 | + |
| 148 | +## Rejected Ideas |
| 149 | + |
| 150 | +* Filtering or flags on attribute definitions to say what kind of target they apply to. By omitting this, a simpler model is provided. It avoids complex questions on how to handle heirarchial classifications, such as that classes are packages, or subroutine parameters are lexical variables. By ensuring that any particular attribute name has at most one definition in any scope, end-users can more easily understand the model provided. |
| 151 | + |
| 152 | +## Open Issues |
| 153 | + |
| 154 | +### SV Type to Represent an Attribute Definition |
| 155 | + |
| 156 | +Attribute definitions need to be SVs in order to live in the lexical pad. There is currently no suitable SV type for this, so one will have to be created. Will it be specific to attributes, or would one type of SV suffice to be shared by various other possible future use-cases of C-level entities visible in the lexical pad, while not intended to be visible to actual perl code as first-class values directly? |
| 157 | + |
| 158 | +### Passing Attribute Definition SV into the Callback |
| 159 | + |
| 160 | +It may be the case that at import time, extra information is attached to the (unique) SV that gets imported into the caller's scope, which is intended for inspection by the attribute callback. Perhaps it makes sense to pass in the definition's SV as another argument to the callback function. |
| 161 | + |
| 162 | +### Passing Lexical Target Information |
| 163 | + |
| 164 | +It would first appear that lexical variables and subroutine parameters can be represented by their PADNAME structure, but notably the padname itself does not actually store the pad offset of the named entity. Perhaps the target argument for these should just be the pad offset of the target entity, leaving the invoked callback to find the offset in the compliing pad itself? |
| 165 | + |
| 166 | +This might suggest that actually the target should be specified as two - or maybe even three - parameters, some of which would be zero / NULL depending on circumstance. |
| 167 | + |
| 168 | +```c |
| 169 | +typedef void PerlAttributeCallback(pTHX_ |
| 170 | + SV *target, GV *targetname, /* for package targets, or NULL/NULL */ |
| 171 | + PADOFFSET targetix, /* for lexical targets, or zero */ |
| 172 | + SV *attrvalue); |
| 173 | +``` |
| 174 | +
|
| 175 | +But perhaps at that point, it makes more sense to have two different callbacks, one for GV-named targets and one for lexicals? |
| 176 | +
|
| 177 | +```c |
| 178 | +struct PerlAttributeCallbacks { |
| 179 | + void (*apply_pkg)(pTHX_ SV *target, SV *targetname, SV *attrvalue); |
| 180 | + void (*apply_lex)(pTHX_ PADOFFSET targetix, SV *attrvalue); |
| 181 | +}; |
| 182 | +``` |
| 183 | + |
| 184 | +Or perhaps one function that takes a distinguishing enum and union type to convey the information: |
| 185 | + |
| 186 | +```c |
| 187 | +enum PerlAttributeTargetKind { |
| 188 | + ATTRTARGET_PKGSCOPED, |
| 189 | + ATTRTARGET_LEXICAL, |
| 190 | +}; |
| 191 | + |
| 192 | +union PerlAttributeTarget { |
| 193 | + struct { SV *sv, GV *namegv; } pkgscoped; |
| 194 | + struct { PADOFFSET padix; } lexical; |
| 195 | +} |
| 196 | + |
| 197 | +typedef void PerlAttributeCallback(pTHX_ |
| 198 | + enum PerlAttributeTargetKind kind, union PerlAttributeTarget target, |
| 199 | + SV *attrvalue); |
| 200 | +``` |
| 201 | +
|
| 202 | +### Split the Pad into Scope + Scratchpad |
| 203 | +
|
| 204 | +While not unique to this proposal, the ongoing increase in use of lexical imports in various parts of perl means that pads in typical programs - both at the file and subroutine level - are getting wider, with more named items in there. Because currently the pads are shared with true per-call lexicals and temporaries, this means that any recursive functions consume more space in unnecessary elements. Every recursive depth of function call requires the entire width of the pad to be cloned for each level. The more "static" elements in the pad, the more wasted space because those elements are not going to vary with depth. |
| 205 | +
|
| 206 | +It would be a nontrivial undertaking, but it may become useful in terms of memory (and CPU cycles) savings to consider splitting the pad into two separate entities. A "scope" pad would exist just once per file or subroutine, and contain lexically-imported named elements such as imported functions or attribute definitions. Separately a "scratchpad" pad would then only contain the lexical variables and temporary values that are needed once for every depth of recursion of the function. By splitting the pad in such a way, it reduces the amount of work that `pp_entersub` has to perform when recursing into functions that have wide pads because now only the true variables-per-call need to be cloned; the static scope would exist just once for all depths. |
| 207 | +
|
| 208 | +### Perl-Level API |
| 209 | +
|
| 210 | +The mechanisms described in this document exist entirely at the C level, intended for XS modules to make use of. It should be possible to provide wrappings of most of the relevant parts, at least for simple cases, for use directly by Perl authors without writing C code. |
| 211 | +
|
| 212 | +Existing experience with [`XS::Parse::Keyword::FromPerl`](https://metacpan.org/pod/XS::Parse::Keyword::FromPerl) shows that while such an interface could easily be provided, in practice it is unlikely anyone would make use of it as it requires understanding details of its operation in sufficient depth that any author would be just as well served by writing the XS code directly. Therefore no such wrapping is yet proposed by this document but could be added later on, either by a core-provided or CPAN module. |
| 213 | +
|
| 214 | +## Copyright |
| 215 | +
|
| 216 | +Copyright (C) 2024, Paul Evans. |
| 217 | +
|
| 218 | +This document and code and documentation within it may be used, redistributed and/or modified under the same terms as Perl itself. |
0 commit comments