File Coverage

lib/Object/Pad/FieldAttr/Checked.xs
Criterion Covered Total %
statement 84 88 95.4
branch 39 80 48.7
condition n/a
subroutine n/a
pod n/a
total 123 168 73.2


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