File Coverage

lib/Object/Pad/FieldAttr/Checked.xs
Criterion Covered Total %
statement 78 83 93.9
branch 38 78 48.7
condition n/a
subroutine n/a
pod n/a
total 116 161 72.0


line stmt bran cond sub pod time code
1             /* You may distribute under the terms of either the GNU General Public License
2             * or the Artistic License (the same terms as Perl itself)
3             *
4             * (C) Paul Evans, 2023 -- leonerd@leonerd.org.uk
5             */
6             #define PERL_NO_GET_CONTEXT
7              
8             #include "EXTERN.h"
9             #include "perl.h"
10             #include "XSUB.h"
11              
12             #include "object_pad.h"
13              
14             struct Data {
15             SV *fieldname;
16             SV *checkname;
17             SV *checkobj;
18             CV *checkcv;
19             };
20              
21 8           static int magic_set(pTHX_ SV *sv, MAGIC *mg)
22             {
23 8           struct Data *data = (struct Data *)mg->mg_ptr;
24              
25             bool ok;
26             {
27 8           dSP;
28              
29 8           ENTER;
30 8           SAVETMPS;
31              
32 8 50         EXTEND(SP, 2);
33 8 50         PUSHMARK(SP);
34 8           PUSHs(sv_mortalcopy(data->checkobj));
35 8           PUSHs(sv); /* Yes we're pushing the SV itself */
36 8           PUTBACK;
37              
38 8           call_sv((SV *)data->checkcv, G_SCALAR);
39              
40 8           SPAGAIN;
41              
42 8 50         ok = SvTRUEx(POPs);
    50          
    0          
    50          
    0          
    0          
    50          
    50          
    50          
    100          
    50          
    100          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
43              
44 8 50         FREETMPS;
45 8           LEAVE;
46             }
47              
48 8 100         if(ok)
49             return 1;
50              
51 1           croak("Field %" SVf " requires a value satisfying :Checked(%" SVf ")",
52 1           SVfARG(data->fieldname), SVfARG(data->checkname));
53              
54             return 1;
55             }
56              
57             static const MGVTBL vtbl = {
58             .svt_set = &magic_set,
59             };
60              
61 10           static bool checked_apply(pTHX_ FieldMeta *fieldmeta, SV *value, SV **attrdata_ptr, void *_funcdata)
62             {
63             SV *checker;
64              
65 10 50         if(mop_field_get_sigil(fieldmeta) != '$')
66 0           croak("Can only apply the :Checked attribute to scalar fields");
67              
68             {
69             dSP;
70              
71 10           ENTER;
72 10           SAVETMPS;
73              
74             /* We can't call eval_sv() because it doesn't preserve the caller's hints
75             * or features. We'll have to emulate it and do different things
76             * https://github.com/Perl/perl5/issues/21415
77             *
78             * Additionally, it also forgets what package we're actually running in
79             * because during compiletime, CopSTASH(PL_curcop == &PL_compiling) isn't
80             * accurate. We need to help it along
81             */
82              
83 10           SAVECOPSTASH_FREE(PL_curcop);
84 10           CopSTASH_set(PL_curcop, PL_curstash);
85              
86             /* We'll turn off strict 'subs' during this code for now, to
87             * support bareword package names as checker expressions
88             */
89 10           SAVEI32(PL_hints);
90 10           PL_hints &= ~HINT_STRICT_SUBS;
91              
92 10           OP *o = newUNOP(OP_ENTEREVAL, G_SCALAR,
93             newSVOP(OP_CONST, 0, SvREFCNT_inc(value)));
94 10 50         OP *start = LINKLIST(o);
95 10           o->op_next = NULL;
96             #ifdef OPpEVAL_EVALSV
97             o->op_private |= OPpEVAL_EVALSV;
98             #endif
99              
100 10           SAVEFREEOP(o);
101              
102             // Now just execute the ops in the list until the end
103 10           SAVEVPTR(PL_op);
104 10           PL_op = start;
105              
106             #ifndef OPpEVAL_EVALSV
107             /* Without OPpEVAL_EVALSV we can only detect compiler errors by
108             * pp_entereval() returning NULL. We'll have to manually run the optree
109             * until we see that to know
110             */
111 30 50         while(PL_op && PL_op->op_type != OP_ENTEREVAL)
    100          
112 20           PL_op = (*PL_op->op_ppaddr)(aTHX);
113 10 50         if(PL_op)
114 10           PL_op = (*PL_op->op_ppaddr)(aTHX); // run the OP_ENTEREVAL
115 10 100         if(!PL_op)
116 1 50         croak_sv(ERRSV);
117             #endif
118 9           CALLRUNOPS(aTHX);
119              
120 9           SPAGAIN;
121              
122             #ifdef OPpEVAL_EVALSV
123             if(!TOPs)
124             croak_sv(ERRSV);
125             #endif
126              
127 9           checker = SvREFCNT_inc(POPs);
128              
129 9 50         FREETMPS;
130 9           LEAVE;
131             }
132              
133             HV *stash;
134 9 100         if(SvROK(checker) && SvOBJECT(SvRV(checker)))
    50          
135 4           stash = SvSTASH(SvRV(checker));
136 5 50         else if(SvPOK(checker) && (stash = gv_stashsv(checker, GV_NOADD_NOINIT)))
    100          
137             ; /* checker is package name */
138             else
139 1           croak("Expected the checker expression to yield an object reference or package name; got %" SVf " instead",
140             SVfARG(checker));
141              
142             GV *methgv;
143 8 100         if(!(methgv = gv_fetchmeth_pv(stash, "check", -1, 0)))
144 1           croak("Expected that the checker expression can ->check");
145 7 50         if(!GvCV(methgv))
146 0           croak("Expected that methgv has a GvCV");
147              
148             struct Data *data;
149 7           Newx(data, 1, struct Data);
150              
151 7           data->fieldname = SvREFCNT_inc(mop_field_get_name(fieldmeta));
152 7           data->checkname = SvREFCNT_inc(value);
153 7           data->checkobj = checker;
154 14           data->checkcv = (CV *)SvREFCNT_inc((SV *)GvCV(methgv));
155              
156 7           *attrdata_ptr = (SV *)data;
157              
158 7           return TRUE;
159             }
160              
161             #define newSLUGOP(idx) S_newSLUGOP(aTHX_ idx)
162             static OP *S_newSLUGOP(pTHX_ int idx)
163             {
164 5           OP *op = newGVOP(OP_AELEMFAST, 0, PL_defgv);
165 5           op->op_private = idx;
166             return op;
167             }
168              
169             #define newLISTOPn(type, flags, ...) S_newLISTOPn(aTHX_ type, flags, __VA_ARGS__, NULL)
170 10           static OP *S_newLISTOPn(pTHX_ OPCODE type, U32 flags, ...)
171             {
172             va_list args;
173 10           va_start(args, flags);
174              
175 10           OP *o = newLISTOP(OP_LIST, 0, NULL, NULL);
176              
177             OP *kid;
178 30 50         while((kid = va_arg(args, OP *)))
    100          
179 20           o = op_append_elem(OP_LIST, o, kid);
180              
181 10           va_end(args);
182              
183 10           return op_convert_list(type, flags, o);
184             }
185              
186 11           static void checked_gen_accessor_ops(pTHX_ FieldMeta *fieldmeta, SV *attrdata, void *_funcdata,
187             enum AccessorType type, struct AccessorGenerationCtx *ctx)
188             {
189             struct Data *data = (struct Data *)attrdata;
190              
191 11           switch(type) {
192             case ACCESSOR_READER:
193             return;
194              
195             case ACCESSOR_WRITER:
196             {
197 15           OP *checkop = newLOGOP(OP_OR, 0,
198             /* checkgv($checker, $_[0]) ... */
199             newLISTOPn(OP_ENTERSUB, OPf_WANT_SCALAR|OPf_STACKED,
200             newSVOP(OP_CONST, 0, SvREFCNT_inc(data->checkobj)),
201             newSLUGOP(0),
202             newSVOP(OP_CONST, 0, SvREFCNT_inc(data->checkcv))),
203             /* ... or die MESSAGE */
204             newLISTOPn(OP_DIE, 0,
205             newSVOP(OP_CONST, 0,
206             newSVpvf("Field %" SVf " requires a value satisfying :Checked(%" SVf ")",
207             SVfARG(mop_field_get_name(fieldmeta)), SVfARG(data->checkname))))
208             );
209              
210 5           ctx->bodyop = op_append_elem(OP_LINESEQ, checkop, ctx->bodyop);
211 5           return;
212             }
213              
214             case ACCESSOR_LVALUE_MUTATOR:
215 0           croak("Cannot currently combine :mutator and :Checked");
216              
217             case ACCESSOR_COMBINED:
218 0           croak("Cannot currently combine :accessor and :Checked");
219              
220             default:
221 0           croak("TODO: Unsure what to do with accessor type %d and :Checked", type);
222             }
223             }
224              
225             /* Object::Pad doesn't currently offer a way to pre-check the values assigned
226             * into fields before assigning them. The best we can do is *temporarily*
227             * apply magic on the field SV itself, check it in the .set callback, then
228             * remove that magic at the end of the constructor.
229             *
230             * This is awkward as it'll still apply the checking to post-:param mutations
231             * inside ADJUST blocks and the like. Fixing that will require more field hook
232             * functions in O:P though
233             */
234              
235 8           static void checked_post_makefield(pTHX_ FieldMeta *fieldmeta, SV *attrdata, void *_funcdata, SV *field)
236             {
237 8           sv_magicext(field, NULL, PERL_MAGIC_ext, &vtbl, (char *)attrdata, 0);
238 8           }
239              
240 7           static void checked_post_construct(pTHX_ FieldMeta *fieldmeta, SV *hookdata, void *_funcdata, SV *field)
241             {
242 7           sv_unmagicext(field, PERL_MAGIC_ext, (MGVTBL *)&vtbl);
243 7           }
244              
245             static const struct FieldHookFuncs checked_hooks = {
246             .ver = OBJECTPAD_ABIVERSION,
247             .flags = OBJECTPAD_FLAG_ATTR_MUST_VALUE,
248             .permit_hintkey = "Object::Pad::FieldAttr::Checked/Checked",
249              
250             .apply = &checked_apply,
251             .gen_accessor_ops = &checked_gen_accessor_ops,
252             .post_makefield = &checked_post_makefield,
253             .post_construct = &checked_post_construct,
254             };
255              
256             MODULE = Object::Pad::FieldAttr::Checked PACKAGE = Object::Pad::FieldAttr::Checked
257              
258             BOOT:
259 4           register_field_attribute("Checked", &checked_hooks, NULL);