June 20, 2021·4 min read·engineeringperlexceptionstooling

Perl 5.35.1 repairs deparsing of scoped catch blocks

Adding a lexical declaration exposed a gap in B::Deparse support for try and catch. The repair shows why new syntax needs tools that understand its compiled scopes.

Adding my $x to an otherwise simple catch block was enough to make B::Deparse fail. The June bug report contrasts two handlers that differ by that declaration. One deparsed successfully. The other produced an internal complaint about the expected second child of a catch operation. The new exception syntax could compile, but a tool inspecting the compiled program did not understand every form of its scope.

The Perl 5.35.1 release notes include the repair and update B::Deparse to 1.57. This is a development-release change to tooling around the experimental syntax. It is not a new exception-handling feature or an announcement that the experiment has finished. Its significance is that ordinary variations in a handler must remain understandable to the interpreter's own inspection tools.

I like Perl for brief programs whose transformations are visible in the source. When an expression becomes less obvious, a tool that shows how Perl parsed it is useful. That makes a deparser defect relevant even when it does not stop the original program from compiling.

A deparser starts from the compiled structure

The module documentation shipped with 5.35.1 describes B::Deparse as a compiler backend. It generates Perl source from the internal structure produced after parsing. This is not a formatter that starts with the original text. Comments and whitespace are not retained, and several source constructions can have the same compiled representation.

That distinction explains both its usefulness and its limits. Reconstructed source can reveal how the parser grouped an expression. It cannot promise to reproduce the author's original spelling. The module's -p option adds parentheses even where precedence does not require them, which helps inspect grouping. Its documentation also acknowledges cases that it cannot reconstruct correctly, including some parts of Perl's own tests.

The failing catch example belongs to this second category. The bug report says the deparser did not handle a catch block with a full ENTER/LEAVE scope. Introducing a lexical declaration produced that form in the reported example. A handler without the declaration took a form the deparser already understood. Both were legitimate source-level uses of the same syntax.

The report therefore supplies a precise reason not to equate similar-looking source with identical internal structure. Scope management can introduce operations that a backend must recognize. A new language construct needs support for those variations even if its shortest demonstration already deparses correctly.

The small failing example explains the repair

This program preserves the relevant shape of the reported failure. It defines a protected block and a handler with a lexical declaration. It is suitable input for the deparser command below.

use feature 'try';
no warnings 'experimental::try';

sub example {
    try {
        1;
    }
    catch ($error) {
        my $temporary;
        $error;
    }
}
perl -MO=Deparse example.pl

The declaration is intentionally uninteresting at the application level. Its role is to require the scope representation that exposed the defect. A complicated network call or a real exception object would obscure that point without making the deparser failure more representative. The issue records the fix as merged on June 14, before its inclusion in this release.

This also separates deparsing from executing an exception path. The example need not throw to exercise the backend's traversal of the handler. A test that only calls the successful branch of the function would ask a different question. Deparsing examines compiled code that execution may never reach during that call.

The experiment's warning category remains explicit in the example because the syntax is experimental. Suppressing that category does not suppress an internal deparser failure or change the compiled scope. It keeps a feature-status diagnostic separate from the tooling behaviour being examined. That separation makes the reproducer easier to interpret.

A second repair improves a failed system call

The release also fixes utime so that calling it on a closed handle correctly sets errno, exposed in Perl through $!. The important change concerns the explanation of a failed operation. Code that receives a failure indication still needs an accurate error value if it is to report what happened. A stale or missing error can make a failure message misleading even when the program notices that the operation did not succeed.

This is independent of the deparser repair, but both concern the reliability of diagnostic information. The handler inspection problem prevented a tool from explaining a valid program. The closed-handle problem prevented a failed operation from supplying its proper error state. Neither requires a new application API to matter to maintainers.

I consider these reasonable priorities immediately after introducing syntax. A language addition creates work in backends and diagnostics as well as in its parser. June's release gives a concrete example of that work through the lexical declaration that changed a catch block's compiled shape.

The version's notes also move scalar assignment to undef from a fatal runtime error to a compile-time error. That catches an invalid assignment before execution and names the operation in the diagnostic. Together with the scoped-handler repair, it gives developers more accurate information at the stage where the relevant problem can already be identified.