Skip to content

annual './TEST -deparse' fixups#24160

Open
iabyn wants to merge 12 commits intobleadfrom
davem/deparse_fixes
Open

annual './TEST -deparse' fixups#24160
iabyn wants to merge 12 commits intobleadfrom
davem/deparse_fixes

Conversation

@iabyn
Copy link
Contributor

@iabyn iabyn commented Feb 3, 2026

This branch fixes up various things so that './TEST -deparse' again runs cleanly. It reflects the accumulated breakage over the past year, as things in the op tree change, new tests and test files are added etc.

In particular, there was quite a bit of fall-out from the recent optimising away of 'stub' ops from empty blocks.

  • This set of changes does not require a perldelta entry.

iabyn added 11 commits February 2, 2026 12:42
Recent work to optimise away empty branches of if/else etc,
unfortunately broke the deparsing of

    if (C1)
        X;
    }
    elsif (C2) {
        Y;
    }

I.e. where there is no final else after the elsif. The new code
assumed that the 'elsif' branch of the if/else was itself
an OP_COND; but in the absence of a final else, and OP_AND is used
instead.
Better explain how the optimised away if/else blocks are handled
and the difference between ?: and if/else.

Also, add a few blank lines for readability.
this:

    for $foo (...) {}

was no longer being deparsed, since a recent change optimised away the
'stub' op making up the body.

This caused Deparse to not spot a valid block top op and mistake it
instead for a 'for' statement modifier. It would then panic because the
loop variable wasn't '$_'.

Also fix the panic - it was just doing confess() without any message.
Do a normal die() instead, with a helpful error message.
'for' was recently enhanced to support

    for my ($x, \$y, $z) (...) { ... }

where the OP_ITER's targ contains both a count of the number of index
variables and a mask indicating which vars are refaliases.

This commit makes Deparse catch up.

Before this commit,

    cd t; ./TEST -deparse op/for-many.t

was failing.
Recent changes optimised away the stub op in empty blocks.
This was causing code like this:

    {
        f(234);
    }
    continue { }

to break the deparser:

    Can't call method "name" on an undefined value at lib/B/Deparse.pm line 6987.

This was due to a long-standing thinko in is_lexical_subs() which was
doing:

    my (@ops) = shift;

when it (presumably) should have been doing:

    my (@ops) = @_;

The pre-existing effect of this thinko was that potentially it might not
spot a lexical sub declaration and thus change whether to wrap the block
in 'do {}'.

The effect after stub become ex-stub was that @_ was now empty and so
$ops[0] was now an undef value.
Handle the recent change where stub ops are optimised out
Since v5.43.1-82-gc0053197fb,

    Reapply "op.c: re-enable coderef-in-stash optimization"

t/op/caller.t no longer passes a round trip through Deparse. This commit
adds it to the list of test files expected to fail.

The issue is that when storing a CV directly in a stash rather than as
part of a new GV, the 'reify GV' code, which later converts the stash
entry from a CV to a GV-to-CV, will set the line/file of the GV to the
location where/when the reification took place, rather than to where the
sub was defined. This doesn't matter 99.999% of the time - I temporarily
hacked Perl_cvgv_from_hek() to always set the line number to 999999 and
no tests failed - but it fails a caller.t deparse round-trip test which
is using '#line' to modify what a BEGIN sub sees as its caller. The
deparsed file doesn't have the correct values in the '#line'.

I can't see any easy way to fix this - I don't think the original
line/file info is stored anywhere - so this commit just skips caller.t
for now.

I've also added some code comments to Perl_cvgv_from_hek() to explain
the implementation leakage.
The same ops, AND/OR/COND_EXPR, are used both for statements such
as:

    if (...) { ... }
    if (...) { ... } else { ... }
    ... if ...;

and expressions, such as:

    ... && ...
    ... ? ... : ...

This commit adds a new private OP flag, OPpSTATEMENT, and makes the
parser set it on such ops when they are derived from if/else etc rather
than from ?:,&&,|| etc.

The flags are currently unused after being set on the ops, but they will
be used by Deparse shortly to stop it from having to guess what form to
use. There are lots of spare private flag bits on those logops, so using
one of them just for a non-essential reason seems reasonable.
Use the OPpSTATEMENT flag, added by the previous commit, to simplify
the deparsing of

    if ($c) { foo() }
    if ($c) { foo() } else { bar() }
    foo() unless $c
    etc

versus

    $c && foo()
    $c ? foo() : bar()
    $c || foo()
    etc

Previously it had to guess which of those two forms to use; now it uses
that flag, which simplifies the code and makes a couple of failing
round-trip test files pass under './TEST -deparse'.
This commit:

- updates the POD in t/test_pl.pod to improve the documentation for the
  refcount_is() test function. It makes it clear that the first argument
  is a *reference* to the thing which is having it's reference count
  compared.

- makes the example in that pod simpler.

- updates the tests in t/test_pl/examples.t which are supposed to test
  that example code snippet in test_pl.pod. It's changed because: first,
  the example code didn't match the test code, second, the example code
  has changed; third, the old test code didn't survive a round-trip
  through './TEST -deparse', since \(1+2) was being constant-folded to
  \1, with a different reference count.
Update the list of skipped files in

    Porting/deparse-skips.txt

New entries have been added due to:

- named params in signatures and classes not yet being deparsed correctly

- constant blessed hash ref not being correctly deparsed

Two entries have been removed since they now pass:

    dist/Data-Dumper/t/trailing_comma.t

due to better deparsing of '&&' vs 'unless'

    cpan/autodie/t/exceptions-smartmatch.t

I'm not sure why that file passes.
Copy link
Contributor

@jkeenan jkeenan left a comment

Choose a reason for hiding this comment

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

Last paragraph in commit message for final commit (64fb988) needs reworking.

@jkeenan
Copy link
Contributor

jkeenan commented Feb 3, 2026

Is this "annual fixup" something which our docs indicate needs to be done on an annual basis? If not, should it be documented? If so, perhaps it should be done in connection with the annual production release?

't/TEST -deparse' - which uses Porting/deparse-skips.txt to decide which
files are expected to fail when run after a round-trip through Deparse -
normally lists any expected-fail test files which unexpectedly
succeeded.

However, it only lists them if at least one other test file failed. Or
to put it another way, if all files expected to pass actually pass, it
doesn't mention that some expected to fail passed too.

This commit changes it so that it *always* lists such unexpected
successes.
@iabyn iabyn force-pushed the davem/deparse_fixes branch from 64fb988 to 5cafb2b Compare February 3, 2026 12:52
@iabyn
Copy link
Contributor Author

iabyn commented Feb 3, 2026

Last paragraph in commit message for final commit (64fb988) needs reworking.

Fixed, rebased and pushed.

@iabyn
Copy link
Contributor Author

iabyn commented Feb 3, 2026

Is this "annual fixup" something which our docs indicate needs to be done on an annual basis? If not, should it be documented? If so, perhaps it should be done in connection with the annual production release?

Ideally it should be done from time to time, especially after commits which add new features or alter the optree. Similarly to running Porting/bench.pl from time to time to see whether anything has slowed down; reducing stderr noise on build; reducing smoke failures etc.

It's something that all porters might do from time to time rather than a specific step to be done by the release manager - who would be unlikely to be in a position to fix the issues anyway, and would likely be looking at the issues to late in the release cycle.

@tonycoz
Copy link
Contributor

tonycoz commented Feb 3, 2026

Ideally it should be done from time to time, especially after commits which add new features or alter the optree. Similarly to running Porting/bench.pl from time to time to see whether anything has slowed down; reducing stderr noise on build; reducing smoke failures etc.

Should we have a CI step that runs TEST -deparse?

@iabyn
Copy link
Contributor Author

iabyn commented Feb 4, 2026 via email

@richardleach
Copy link
Contributor

In particular, there was quite a bit of fall-out from the recent optimising away of 'stub' ops from empty blocks.

Apologies for that, Dave. Thanks for fixing it up.

I go through spells of remembering about ./TEST -deparse and then forget it again. I'll try to embed it into my workflow this year!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants