File Coverage

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