Skip to content

Conversation

@leonerd
Copy link
Contributor

@leonerd leonerd commented Oct 22, 2025

This adds a major new ability to subroutine signatures, allowing callers to pass parameters by name/value pairs rather than by position.

sub f ($x, $y, :$alpha, :$beta = undef) { ... }

f( 123, 456, alpha => 789 );

Originally specified in

https://github.com/Perl/PPCs/blob/main/ppcs/ppc0024-signature-named-parameters.md

This feature is currently considered experimental.

  • This set of changes requires a perldelta entry, and it is included.

@leonerd leonerd force-pushed the signature-named-parameters branch 2 times, most recently from f8ecfa6 to a82ff80 Compare October 22, 2025 16:39
pp.c Outdated
else {
// TODO: Consider collecting up all the names of unrecognised
// in one string
croak("Unrecognized named parameter '%s' to subroutine '%" SVf "'",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you change these croaks to Perl_croak_caller() the errors will include the line number of the caller rather than where the subroutine is defined which would be way more helpful (and in line with what signatures do without named parameters)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

namepv may or may not be utf8 encoded, and this message doesn't account for that.

croak_caller("Unrecognized named parameter '" UTF8f "' to subroutine '%" SVf "'",
                    UTF8fARG(SvUTF8(name), namepv), S_find_runcv_name());

If the above changes to fetch the UTF8 name this should pass true instead of SvUTF8(name).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@wolfsage
Copy link
Contributor

It seems that a slurp '@' doesn't allow you to pass extra parameters:

sub foo (:$x, @) {}
sub bar (:$y, @extra) {}

bar(y => 1, x => "a");
# This one fails with Unrecognized named parameter 'a' to subroutine 'main::foo'
foo(x => 1, "a");

@wolfsage
Copy link
Contributor

wolfsage commented Oct 22, 2025

Extra parameters aren't detected if they have no key value pair. Consider:

sub foo (:$x) {}
foo(x => 1, bar => 1); # Unrecognized named parameter 'bar' to subroutine 'main::foo'

vs

sub foo (:$x) {}
foo(x => 1, 1); # Odd name/value argument for subroutine 'main::foo'

... compared with normal sub sigs which gives "Too many arguments for subroutine 'main::foo'"

@wolfsage
Copy link
Contributor

This recurses into madness consuming memory:

sub foo (:$x, @rest) {}
foo(x => 1, "oh");
Use of uninitialized value in signature processing at sigs line 7.
Use of uninitialized value in signature processing at sigs line 7.
Use of uninitialized value in signature processing at sigs line 7.
Use of uninitialized value in signature processing at sigs line 7.
Use of uninitialized value in signature processing at sigs line 7.
Use of uninitialized value in signature processing at sigs line 7.
Use of uninitialized value in signature processing at sigs line 7.

if(!named->is_required || !SvPADSTALE(PAD_SVl(named->padix)))
continue;

// TODO: Consider collecting up all the names of missing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes please (and unrecognized ones above...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix those TODOs in a follow-up commit once the initial bit is merged. That's more of a "nice-to-have" feature.


/* For now, build the named param array in iteration order. We'll
* sort it at the end.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it actually sorted? If I do sub foo (:$z, :$x) {} foo(); the error I get is for 'z' missing, but I'd expect 'x'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah it's currently not sorted at all. Eventually it'll be sorted by order of the hash value - so not something that's useful for end users.

@wolfsage
Copy link
Contributor

This recurses into madness consuming memory:

sub foo (:$x, @rest) {}
foo(x => 1, "oh");

From reading the docs and the code, it seems like the intent is.. if you have named parameters then anything slurpy at the end should be key / values pairs even if it's @foo.

I'm not sure I agree with this. I think if you want k/v pairs still, use %foo, otherwise anything extra can be ... whatever. This is consistent with normal sub signatures and least surprising.

@wolfsage
Copy link
Contributor

With all of that said, I'm very excited for this! Thanks again!

op.c Outdated
struct op_multiparam_named_aux *named = aux->named + namedix;
namedix++;

named->namepv = savepv(namepv);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is being stored in an op, shouldn't this be savesharedpv(namepv)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

op.c Outdated
if(aux->named) {
for(size_t namedix = 0; namedix < aux->n_named; namedix++) {
struct op_multiparam_named_aux *named = aux->named + namedix;
Safefree(named->namepv);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To match allocating namepv with savesharedpv() mentioned below, this should PerlMemShared_free().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@wolfsage
Copy link
Contributor

wolfsage commented Oct 22, 2025

sub foo(:&cat) {} # A signature parameter must start with '$', '@' or '%'

If we detect it's a named parameter can we give a more accurate error message? The above suggests :@cat is legal but of course:

Only a scalar parameter may be named

if (UNLIKELY(SvGMAGICAL(key)))
key = sv_mortalcopy(key);
STRLEN namelen;
const char *namepv = SvPV(name, namelen);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

namepv may or may not be utf8 encoded, but named->namepv always is.

This isn't a problem for ASCII names, which are all that are allowed without use utf8, but the caller could specify the name as a string and expect Latin-1 encoding (ie. the strings would be equal in a perl string sense, but the matching here would not match.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@mauke
Copy link
Contributor

mauke commented Oct 23, 2025

Something odd is happening with eval:

$ ./perl -Ilib -wE 'sub (:$x, $y) {}'
Named parameters in signatures are experimental at -e line 1.
Positional parameter follows named parameter at -e line 1, near "$y) "
Execution of -e aborted due to compilation errors.
$ ./perl -Ilib -wE 'my $f = eval(q{ sub (:$x, $y) {} }); say $f; $f->()'
CODE(0x5712557d23f8)
Too few arguments for subroutine 'main::__ANON__' (got 0; expected 2) at -e line 1.

If (:$x, $y) is a compilation error, why doesn't the eval return undef?

@mauke
Copy link
Contributor

mauke commented Oct 23, 2025

$ ./perl -Ilib -E 'sub f(:$x, :$x) {} say 42'
Named parameters in signatures are experimental at -e line 1.
42

I think declaring the same named parameter twice in one signature should be a compiler error.

@mauke
Copy link
Contributor

mauke commented Oct 23, 2025

$ ./perl -Ilib -wE 'sub f($x = 1, :$y) {} say 42'
Named parameters in signatures are experimental at -e line 1.
42

I think this should be a compiler error as well. It doesn't make sense to have optional positional parameters if there are required named parameters in the same signature because you can never call f without explicitly giving a value for $x anyway.

pod/perlsub.pod Outdated
foo(%params);

As with positional parameters, named parameters may be declared with a
defaulting expression to provide a value to assign into the varible if the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"variable"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@leonerd leonerd force-pushed the signature-named-parameters branch from a82ff80 to e929c00 Compare October 23, 2025 16:02
@leonerd
Copy link
Contributor Author

leonerd commented Oct 23, 2025

@wolfsage

It seems that a slurp '@' doesn't allow you to pass extra parameters:

Fixed.

This recurses into madness consuming memory:

Fixed. This was a while loop that decremented 2 from argc and waited until it was zero. Of course if it started at an odd number that never happened; it wrapped around to overflow and went off the rails there.

... compared with normal sub sigs which gives "Too many arguments for subroutine 'main::foo'"

Ahyes, that's because the argc-parity check comes quite early on. Here, it realises the parity of argc is wrong, so complains about that before it does anything else.

I'm not sure I agree with this. I think if you want k/v pairs still, use %foo, otherwise anything extra can be ... whatever. This is consistent with normal sub signatures and least surprising.

This is now fixed and a test added. Specifically, with a slurpy array and a single spare value, it checks only that value is pushed, and not an additional undef.

If we detect it's a named parameter can we give a more accurate error message? The above suggests :@cat is legal but of course:

Fixed, by adding a new message.

@leonerd
Copy link
Contributor Author

leonerd commented Oct 23, 2025

@mauke

I think declaring the same named parameter twice in one signature should be a compiler error.

Fixed.

I think this should be a compiler error as well. It doesn't make sense to have optional positional parameters if there are required named parameters in the same signature because you can never call f without explicitly giving a value for $x anyway.

Fixed.

Something odd is happening with eval:

Hrmmm. Something tricky, probably because the yyerror() function doesn't actually have croak-like behaviour but just sets an error flag and carries on anyway. I expect something somewhere isn't checking correctly in that case. I'll have a look shortly.

@leonerd
Copy link
Contributor Author

leonerd commented Oct 23, 2025

Reviewers: Thanks all for the many and varied comments.

Besides one awkward bug to do with eval(), I think I've addressed them all. For now, I've left the fixes in various "fixup" commits, but I'll squash those down in the main one before merge.

@leonerd leonerd added the squash-before-merge Author must squash the commits down before merging to blead label Oct 23, 2025
@leonerd leonerd force-pushed the signature-named-parameters branch from e929c00 to 0928cf9 Compare October 23, 2025 16:39
@leonerd leonerd force-pushed the signature-named-parameters branch from 0928cf9 to a3cdd57 Compare October 23, 2025 22:55
@leonerd
Copy link
Contributor Author

leonerd commented Oct 24, 2025

Something odd is happening with eval:

$ ./perl -Ilib -wE 'sub (:$x, $y) {}'
Named parameters in signatures are experimental at -e line 1.
Positional parameter follows named parameter at -e line 1, near "$y) "
Execution of -e aborted due to compilation errors.
$ ./perl -Ilib -wE 'my $f = eval(q{ sub (:$x, $y) {} }); say $f; $f->()'
CODE(0x5712557d23f8)
Too few arguments for subroutine 'main::__ANON__' (got 0; expected 2) at -e line 1.

If (:$x, $y) is a compilation error, why doesn't the eval return undef?

Actually, this is way weirder. I don't think it is related to the eval(), I think it's about context of the anonymous sub expression.

Having added some debug prints, I can get this:

$ ./perl -E 'my $sub = sub (:$x, :$y) {}; say $sub'
subsignature append positional padix=1<$x>
subsignature append named padix=2<$y> name=<y>
Named parameters in signatures are experimental at -e line 1.
CODE(0x5583e39e1358)

leo@vel:~/src/bleadperl/perl [git]
$ ./perl -E 'my $sub; $sub = sub (:$x, :$y) {}; say $sub'
subsignature append positional padix=1<$x>
subsignature append named padix=2<$y> name=<y>
Named parameters in signatures are experimental at -e line 1.
CODE(0x55d4124f35a8)

leo@vel:~/src/bleadperl/perl [git]
$ ./perl -E 'my $sub; sub (:$x, :$y) {}; say $sub'
subsignature append named padix=1<$x> name=<x>
Named parameters in signatures are experimental at -e line 1.
subsignature append named padix=2<$y> name=<y>

It seems that when the anonymous sub is on RHS of an assignment expression, the first param is treated as positional, not named. Most odd indeed.

@leonerd leonerd force-pushed the signature-named-parameters branch from a3cdd57 to e287ee0 Compare October 24, 2025 15:28
@leonerd
Copy link
Contributor Author

leonerd commented Oct 24, 2025

Last bug now fixed by e287ee0 - turned out to be an action block in perly.y leaving the llval uninitialised, which fortunately most of the time happened to be true, but in some cases was false and upset the logic.

I believe that's all the issues fixed now

@jkeenan
Copy link
Contributor

jkeenan commented Oct 24, 2025

Last bug now fixed by e287ee0 - turned out to be an action block in perly.y leaving the llval uninitialised, which fortunately most of the time happened to be true, but in some cases was false and upset the logic.

I believe that's all the issues fixed now

I tested the pull request with Perl5::TestEachCommit. All commits PASSed on their own. LGTM.

@leonerd leonerd force-pushed the signature-named-parameters branch from e0d65bb to 7676a6b Compare October 25, 2025 12:17
Copy link
Contributor

@rwp0 rwp0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made a review on docs' (perlsub.pod) spelling etc

pod/perlsub.pod Outdated
passed by the caller when the subroutine is invoked. Whereas positional
parameters take their values from the corresponding position in the list
passed by the caller, named parameters are matched by name-value pairs
from the caller, where the name matches the variable name of the parameter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think , (the comma) is not needed here before "where".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It probably reads better as a separate sentence even.

Named parameters in signatures are currently considered B<experimental>, and
their behaviour may change in future releases of Perl. Uses of named
parameters in a signature will currently emit a warning in the
C<experimental::signature_named_parameters> category.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could instead be a link to that category ie. within the pod of the experimental pragma (so that the users could conveniently read more if needed).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

experimental isn't (exactly) a core module, but also its documentation doesn't have separate sections per warning category. Nor does warnings.

pod/perlsub.pod Outdated

To invoke this function, the caller must pass pairs of names and values to
provide values for the parameters. Each name must match the name of one of
the variables, omimtting its leading C<$> sigil. The order of these values
Copy link
Contributor

@rwp0 rwp0 Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"omitting", without the second M, also as previously I find the comma to be unnecessary and confusing here too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

pod/perlsub.pod Outdated
To invoke this function, the caller must pass pairs of names and values to
provide values for the parameters. Each name must match the name of one of
the variables, omimtting its leading C<$> sigil. The order of these values
is not important; both of the following lines will cause the same behaviour.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected : (colon) rather than . (comma) in the end to introduce the following lines (code samples).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment on lines +587 to +594
=item *

zero or more mandatory positional scalar parameters

=item *

zero or more optional positional scalar parameters

Copy link
Contributor

@rwp0 rwp0 Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe merge into a single list item as with the following named scalar parameters using OR (ie. mandatory or optional) for consistency in the Summary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No; the entire point here is that there's zero-or-more mandatory, followed by zero-or-more optional, in two distinct groups. They aren't all mixed together.

Copy link
Member

@karenetheridge karenetheridge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

Just a few minor tweaks to some of the doc wording.

pp.c Outdated

/* namepv / namelen are always UTF-8 */
STRLEN namelen;
const char *namepv = SvPVutf8(name, namelen);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the callers SV is modifiable and isn't special (it won't overwrite references or GVs for example) this will upgrade it in place, modifying the caller's SV.

If the SV isn't utf8 you could use bytes_to_utf8_free_me() or bytes_to_utf8_temp_pv() to get the UTF8 version cheaply:

const char *namepv = SvPV(name, namelen);
if (!SvUTF8(name))
    namepv = bytes_to_utf8_temp_pv(namepv, &namelen);

which saves the cost of making a new SV, and bytes_to_utf8_temp_pv() is smart about invariants (it will return namepv if the string contains only invariants).

The _temp_pv() variant uses SAVEFREEPV() to ensure the namepv is release (if allocated), alternatively you can use the _free_me() variant where you need to Safefree() the allocation, but the croak() makes that difficult to use here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahyes, now changed and added a test to check that non-UTF8 SVs from the caller are not upgraded like that any more.

@leonerd leonerd force-pushed the signature-named-parameters branch from 0cb38cf to 37a1016 Compare October 27, 2025 11:31
Co-authored-by: Karen Etheridge <ether@cpan.org>
@leonerd leonerd force-pushed the signature-named-parameters branch 2 times, most recently from 051148d to 2786df0 Compare October 27, 2025 12:40
@leonerd leonerd force-pushed the signature-named-parameters branch from 2786df0 to 7cc7882 Compare October 27, 2025 12:41
@leonerd
Copy link
Contributor Author

leonerd commented Oct 27, 2025

Awesome!

Just a few minor tweaks to some of the doc wording.

Thanks, now all applied.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

squash-before-merge Author must squash the commits down before merging to blead

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants