Perl / perl5

🐪 The Perl programming language
https://dev.perl.org/perl5/
Other
1.9k stars 540 forks source link

[EXPERIMENT] defer {} syntax #17949

Open leonerd opened 4 years ago

leonerd commented 4 years ago

(copied from https://www.nntp.perl.org/group/perl.perl5.porters/2020/06/msg257611.html )

I'm trying to simplify the design of a try/catch syntax out of Syntax::Keyword::Try so it can be moved into core perl. One thing I think we can get rid of is the finally keyword, in favour of instead inventing a new LEAVE phaser block, similar to the Raku ones.

In summary: I'd like to add LEAVE with syntax which looks like another phaser such as END, but runs at the time you leave the block it is placed in:

  sub f {
    print 1;
    LEAVE { print 3; }

    print 2;
  }

  f();

Will output

123

Furthermore, a LEAVE phaser inside something like a foreach loop would run every time you leave the block in order to run a new one:

  foreach (qw( a b c )) {
    print "Start($_)";
    LEAVE { print "End($_)"; }

    more code here...
  }

will output

Start(a)End(a)Start(b)End(b)Start(c)End(c)

Seems simple enough.

It is important to note that a LEAVE block is similar to an END block, in that the mere presence of that syntax is enough to queue it for being run, regardless of whether the line of code it's on was actually "reached". Thus, just as in

  exit 0;
  END { say "Ending the program now" }

still prints, so too would

  sub f {
    return;
    LEAVE { say "Leaving the function now" }
  }

If we had a LEAVE phaser (totally independent of any thoughts of try/catch), it reduces the scope of that work considerably and makes it easier to work out how to implement.

Thoughts?

Grinnz commented 4 years ago

FTR, prior work on CPAN includes https://metacpan.org/pod/Scope::OnExit, https://metacpan.org/pod/Scope::Cleanup, and various scope guards like https://metacpan.org/pod/Scope::Guard

leonerd commented 4 years ago

Further discussions about LEAVE have lead to an interesting question - What is the behaviour of exceptions thrown from them?

In a simple case such as

  sub f {
    return "123";
    LEAVE { die "456" }
  }

It is relatively easy to argue the case that this should die with "456 at ...", but complications come when the LEAVE block is being executed during stack unwind because another exception is being thrown.

  sub g {
    die "123";
    LEAVE { die "456" }
  }

It is fairly agreeable that invoking g() should die in some manner, but exactly how?

Taking a look around other things: Within perl we have many CPAN variations on this theme, almost all of which ultimately rely on DESTROY methods being invoked on objects captured by block-scoped variables. Because exceptions during DESTROY are turned into warnings, necessarily all of these must act the same way. Thus the 456 would be warned about but otherwise invisible to callers, who would receive the 123 exception.

My own Syntax::Keyword::Try offers a similar feature in the form of try/finally blocks. These use the core SAVEDESTRUCTOR_X feature which could be capable of propagating the exception but currently chooses not to due to some unanswered questions I have about how that would be represented.

Some other languages choose different ideas. For example, Java takes the latest exception and totally discards an earlier one (https://programming.guide/java/try-finally.html). Python appears to do similar, judging from some StackOverflow posts (though at present I am unable to locate specific words in the actual spec). Suggestions are that C# and JavaScript also do this.

In the case of the 123+456 case above, I can basically see three possible choices of behaviour:

a) Warn about 456, and propagate 123 to caller.

This is the current behaviour of SKT's try/finally, and also equivalent to how other CPAN options work)

b) Warn about 123, and propagate 456 to caller.

This is what most other langs do, except they don't even have a mechanism to warn about 123, so all information about 123 is lost entirely!

c) Combine 123 and 456 into a new "double fault" exception representation

This one would not be possible without a more standard form in which to represent exceptions in core perl. But see also https://github.com/Perl/perl5/issues/17951

(also posted to perl5-porters@ https://www.nntp.perl.org/group/perl.perl5.porters/2020/07/msg257949.html)

Grinnz commented 3 years ago

For those following along at home, this idea has been renamed back to FINALLY and is being protoyped on CPAN: https://metacpan.org/pod/Syntax::Keyword::Finally

dur-randir commented 3 years ago

I think this really lacks flexibility without being able to change a 'return' value from a function, unlike a true 'finally' block.

Grinnz commented 3 years ago

I've never heard of such a feature in finally blocks. Do you have examples of prior art that include this feature? I don't find it particularly compelling, regardless; given Perl's notion of return context and dynamic optrees, this sounds quite complex, and I prefer the assurance that the block will indeed not alter any return values.

dur-randir commented 3 years ago

For example, java, see http://tpcg.io/PyA94RXu.

Grinnz commented 3 years ago

Notable from the Syntax::Keyword::Finally docs, as this differs a bit from the original proposal for LEAVE:

The operation can be considered a little similar to an END block, but with the following key differences:

  • A FINALLY block runs at the time that execution leaves the block it is declared inside, whereas an END block runs at the end time of the entire program regardless of its location.
  • A FINALLY block is invoked at the time its containing scope has finished, which means it might run again if the block is entered again later in the program. An END block will only ever run once.
  • A FINALLY block will only take effect if execution reaches the line it is declared on; if the line is not reached then nothing happens. An END block will always be invoked once declared, regardless of the dynamic extent of execution at runtime.
Grinnz commented 3 years ago

Another update: the proposal is now being renamed to lowercase 'defer': https://metacpan.org/pod/Syntax::Keyword::Defer

leonerd commented 3 years ago

I've now turned this issue into a draft RFC at https://github.com/leonerd/Perl-RFCs/blob/rfc0004/rfcs/rfc0004.md

leonerd commented 3 years ago

https://github.com/Perl/RFCs/pull/2

leonerd commented 3 years ago

The initial PR is now merged to blead. There are still some issues with it in odd situations, such as jumping out of a deferred block with goto &func, or jumping in with a regular goto. These will need fixing.

leonerd commented 2 years ago

defer {...} was first released in Perl v5.36 as an experimental feature.

There are currently no planned extensions to this experiment. It is now currently waiting out its bug-discovery period.