Skip to content

Commit 2bb873b

Browse files
committed
class.c: Define a :writer attribute, applicable to scalar fields only
1 parent f254d77 commit 2bb873b

File tree

5 files changed

+186
-5
lines changed

5 files changed

+186
-5
lines changed

class.c

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,104 @@ apply_field_attribute_reader(pTHX_ PADNAME *pn, SV *value)
10551055
CvIsMETHOD_on(cv);
10561056
}
10571057

1058+
/* If '@_' is called "snail", then elements of it can be called "slugs"; i.e.
1059+
* snails out of their container. */
1060+
#define newSLUGOP(idx) S_newSLUGOP(aTHX_ idx)
1061+
static OP *
1062+
S_newSLUGOP(pTHX_ IV idx)
1063+
{
1064+
assert(idx >= 0 && idx <= 255);
1065+
OP *op = newGVOP(OP_AELEMFAST, 0, PL_defgv);
1066+
op->op_private = idx;
1067+
return op;
1068+
}
1069+
1070+
static void
1071+
apply_field_attribute_writer(pTHX_ PADNAME *pn, SV *value)
1072+
{
1073+
char sigil = PadnamePV(pn)[0];
1074+
if(sigil != '$')
1075+
croak("Cannot apply a :writer attribute to a non-scalar field");
1076+
1077+
if(value)
1078+
SvREFCNT_inc(value);
1079+
else {
1080+
/* Default to "set_" . name minus the sigil */
1081+
value = newSVpvs("set_");
1082+
sv_catpvn_flags(value, PadnamePV(pn) + 1, PadnameLEN(pn) - 1,
1083+
PadnameUTF8(pn) ? SV_CATUTF8 : 0);
1084+
}
1085+
1086+
if(!valid_identifier_sv(value))
1087+
croak("%" SVf_QUOTEDPREFIX " is not a valid name for a generated method", value);
1088+
1089+
PADOFFSET fieldix = PadnameFIELDINFO(pn)->fieldix;
1090+
1091+
I32 floor_ix = start_subparse(FALSE, 0);
1092+
SAVEFREESV(PL_compcv);
1093+
1094+
I32 save_ix = block_start(TRUE);
1095+
1096+
PADOFFSET padix;
1097+
1098+
padix = pad_add_name_pvs("$self", 0, NULL, NULL);
1099+
assert(padix == PADIX_SELF);
1100+
1101+
padix = pad_add_name_pvn(PadnamePV(pn), PadnameLEN(pn), 0, NULL, NULL);
1102+
intro_my();
1103+
1104+
OP *methstartop;
1105+
{
1106+
UNOP_AUX_item *aux;
1107+
aux = (UNOP_AUX_item *)PerlMemShared_malloc(
1108+
sizeof(UNOP_AUX_item) * (2 + 2));
1109+
1110+
UNOP_AUX_item *ap = aux;
1111+
(ap++)->uv = 1; /* fieldcount */
1112+
(ap++)->uv = fieldix; /* max_fieldix */
1113+
1114+
(ap++)->uv = padix;
1115+
(ap++)->uv = fieldix;
1116+
1117+
methstartop = newUNOP_AUX(OP_METHSTART, 0, NULL, aux);
1118+
}
1119+
1120+
OP *argcheckop;
1121+
{
1122+
struct op_argcheck_aux *aux = (struct op_argcheck_aux *)
1123+
PerlMemShared_malloc(sizeof(*aux));
1124+
1125+
aux->params = 1;
1126+
aux->opt_params = 0;
1127+
aux->slurpy = 0;
1128+
1129+
argcheckop = newUNOP_AUX(OP_ARGCHECK, 0, NULL, (UNOP_AUX_item *)aux);
1130+
}
1131+
1132+
OP *assignop = newBINOP(OP_SASSIGN, 0,
1133+
newSLUGOP(0),
1134+
newPADxVOP(OP_PADSV, OPf_MOD|OPf_REF, padix));
1135+
1136+
OP *retop = newLISTOP(OP_RETURN, 0,
1137+
newOP(OP_PUSHMARK, 0),
1138+
newPADxVOP(OP_PADSV, 0, PADIX_SELF));
1139+
1140+
OP *ops = newLISTOPn(OP_LINESEQ, 0,
1141+
methstartop,
1142+
argcheckop,
1143+
assignop,
1144+
retop,
1145+
NULL);
1146+
1147+
SvREFCNT_inc(PL_compcv);
1148+
ops = block_end(save_ix, ops);
1149+
1150+
OP *nameop = newSVOP(OP_CONST, 0, value);
1151+
1152+
CV *cv = newATTRSUB(floor_ix, nameop, NULL, NULL, ops);
1153+
CvIsMETHOD_on(cv);
1154+
}
1155+
10581156
static struct {
10591157
const char *name;
10601158
bool requires_value;
@@ -1068,6 +1166,10 @@ static struct {
10681166
.requires_value = false,
10691167
.apply = &apply_field_attribute_reader,
10701168
},
1169+
{ .name = "writer",
1170+
.requires_value = false,
1171+
.apply = &apply_field_attribute_writer,
1172+
},
10711173
{ NULL, false, NULL }
10721174
};
10731175

pod/perlclass.pod

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,33 @@ context.
260260

261261
scalar $instance->users;
262262

263+
=head3 :writer
264+
265+
A field with a C<:writer> attribute will generate a writer accessor method
266+
automatically. The generated method will have a signature that consumes
267+
exactly one argument, and its body will assign that scalar argument to the
268+
field, and return the invocant object itself.
269+
270+
field $s :writer;
271+
272+
# Equivalent to
273+
field $s;
274+
method set_s($new) { $s = $new; return $self; }
275+
276+
By default the accessor method will have the name of the field minus the
277+
leading sigil with the string C<set_> prefixed to it, but a different name
278+
can be specified in the attribute's value.
279+
280+
field $x :writer(write_x);
281+
282+
# Generates a method
283+
method write_x ($new) { ... }
284+
285+
Curerently, writer accessors can only be applied to scalar fields. Attempts
286+
to apply this attribute to a non-scalar field will result in a fatal exception
287+
at compile-time. This may be relaxed in a future version to allow writers on
288+
array or hash fields. For now, these will have to be created manually.
289+
263290
=head2 Method attributes
264291

265292
None yet.

pod/perldiag.pod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,13 @@ checking. Alternatively, if you are certain that you're calling the
699699
function correctly, you may put an ampersand before the name to avoid
700700
the warning. See L<perlsub>.
701701

702+
=item Cannot apply a :writer attribute to a non-scalar field
703+
704+
(F) An attempt was made to use the C<:writer> attribute on a field that is
705+
not a scalar (i.e. an array or hash). At the present version, these are only
706+
permitted on scalar fields. You will have to manually create a writer
707+
accessor method yourself.
708+
702709
=item Cannot assign :param(%s) to field %s because that name is already in use
703710

704711
(F) An attempt was made to apply a parameter name to a field, when the name

t/class/accessor.t

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,42 @@ no warnings 'experimental::class';
3737
'Failure from argument to accessor');
3838
}
3939

40-
# Alternative names
40+
# writer accessors on scalars
4141
{
4242
class Testcase2 {
43-
field $f :reader(get_f) = "value";
43+
field $s :reader :writer = "initial";
44+
}
45+
46+
my $o = Testcase2->new;
47+
is($o->s, "initial", '$o->s accessor before modification');
48+
is($o->set_s("new-value"), $o, '$o->set_s accessor returns instance');
49+
is($o->s, "new-value", '$o->s accessor after modification');
50+
51+
# Write accessor wants exactly one argument
52+
ok(!eval { $o->set_s() },
53+
'Reader accessor fails with no argument');
54+
like($@, qr/^Too few arguments for subroutine \'Testcase2::set_s\' \(got 0; expected 1\) at /,
55+
'Failure from argument to accessor');
56+
ok(!eval { $o->set_s(1, 2) },
57+
'Reader accessor fails with 2 arguments');
58+
like($@, qr/^Too many arguments for subroutine \'Testcase2::set_s\' \(got 2; expected 1\) at /,
59+
'Failure from argument to accessor');
60+
}
61+
62+
# Alternative names
63+
{
64+
class Testcase3 {
65+
field $f :reader(get_f) :writer(write_f) = "value";
4466
}
4567

46-
is(Testcase2->new->get_f, "value", 'accessor with altered name');
68+
is(Testcase3->new->get_f, "value",
69+
'read accessor with altered name');
70+
ok(Testcase3->new->write_f("new"),
71+
'write accessor with altered name');
4772

48-
ok(!eval { Testcase2->new->f },
73+
ok(!eval { Testcase3->new->f },
4974
'Accessor with altered name does not also generate original name');
50-
like($@, qr/^Can't locate object method "f" via package "Testcase2" at /,
75+
like($@, qr/^Can't locate object method "f" via package "Testcase3" at /,
5176
'Failure from lack of original name accessor');
5277
}
5378

t/lib/croak/class

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,23 @@ class XXX {
165165
}
166166
EXPECT
167167
"abc-def" is not a valid name for a generated method at - line 6.
168+
########
169+
# Invalid method name for :writer attribute
170+
use v5.36;
171+
use feature 'class';
172+
no warnings 'experimental::class';
173+
class XXX {
174+
field $x :writer(set-abc-def);
175+
}
176+
EXPECT
177+
"set-abc-def" is not a valid name for a generated method at - line 6.
178+
########
179+
# Writer on non-scalar field
180+
use v5.36;
181+
use feature 'class';
182+
no warnings 'experimental::class';
183+
class XXX {
184+
field @things :writer;
185+
}
186+
EXPECT
187+
Cannot apply a :writer attribute to a non-scalar field at - line 6.

0 commit comments

Comments
 (0)