perl.gg / hidden-gems

<!-- category: hidden-gems -->

B::Deparse - What Perl Actually Compiles Your Code Into

2026-05-03

You write Perl. Perl reads your Perl. Then Perl compiles it into something that may look nothing like what you wrote.

Want to see what Perl actually thinks your code says?

$ perl -MO=Deparse -e 'print "hello" if $x'
print 'hello' if $x; -e syntax OK
Okay, that one was boring. Try something weirder:
$ perl -MO=Deparse -e 'print "hello" unless !$x'
print 'hello' if $x; -e syntax OK
Perl just simplified unless !$x into if $x. It saw through the double negative and normalized it. The code you wrote and the code Perl compiled are different. B::Deparse shows you the compiled version.

Part 1: BASIC USAGE

B::Deparse is a backend module that takes Perl's compiled op-tree (the internal representation after parsing) and converts it back into Perl source code. The canonical form. The way Perl understood it.
$ perl -MO=Deparse script.pl
That is the basic invocation. -MO=Deparse tells Perl to compile the script, pass the op-tree to B::Deparse, and print the result instead of running the program.

Your script does not execute. It only compiles. B::Deparse shows you what the compiler produced.

$ perl -MO=Deparse -e 'for (1..10) { print }'
foreach $_ (1 .. 10) { print $_; } -e syntax OK
Perl filled in the implicit $_ that you left out. It made the default variable explicit. B::Deparse shows you what was always there but never written.

Part 2: REVEALING IMPLICIT BEHAVIORS

Perl is full of defaults. Default variables, default filehandles, default behaviors. B::Deparse makes them all visible.
$ perl -MO=Deparse -e 'while (<>) { chomp; print if /foo/ }'
while (defined($_ = <ARGV>)) { chomp $_; print $_ if /foo/; } -e syntax OK
Look at everything Perl added:

Every implicit default is now explicit. This is what Perl was actually doing all along. You just did not have to type it.

Part 3: DEOBFUSCATING CODE

Found some obfuscated Perl on the internet? Someone's JAPH that looks like line noise? Feed it to B::Deparse.
$ perl -MO=Deparse -e '$_="Just another Perl hacker,";print'
$_ = 'Just another Perl hacker,'; print $_; -e syntax OK
B::Deparse normalizes the code. Adds whitespace. Makes implicit arguments explicit. Turns clever tricks into readable code.

It will not fully deobfuscate runtime string eval tricks (because those happen at runtime, not compile time), but it handles syntactic obfuscation beautifully.

$ perl -MO=Deparse -e 'print+("hello"x3)'
print 'hello' x 3; -e syntax OK
The + after print (which resolves the ambiguity between print as a function and print as a list operator) disappears. B::Deparse understood the intent and produced the cleaner form.

Part 4: UNDERSTANDING OPERATOR PRECEDENCE

Not sure how Perl parses a complex expression? B::Deparse shows you where the parentheses go.
$ perl -MO=Deparse -e 'print 1 + 2 * 3'
print 7; -e syntax OK
Perl computed the constant expression at compile time. It knew 1 + 2 * 3 is always 7, so it folded it into a constant. B::Deparse shows you the optimized result.

For non-constant expressions:

$ perl -MO=Deparse -e 'print $a + $b * $c'
print $a + $b * $c; -e syntax OK
Not super helpful here. Use the -p flag for explicit parentheses:
$ perl -MO=Deparse,-p -e 'print $a + $b * $c'
print(($a + ($b * $c))); -e syntax OK
Now you can see that $b * $c happens first. The -p flag wraps every subexpression in parentheses, making the precedence tree visible.

Part 5: THE -p FLAG FOR EXTRA PARENTHESES

The -p flag is your best friend for understanding precedence in complex expressions:
$ perl -MO=Deparse,-p -e '$x = $a || $b && $c'
($x = ($a || ($b && $c))); -e syntax OK
&& binds tighter than ||. The parentheses prove it. No more guessing, no more checking the operator precedence table. B::Deparse shows you the actual parse tree.

Another good one:

$ perl -MO=Deparse,-p -e 'not $a and $b or $c'
(((not $a) and $b) or $c); -e syntax OK
not binds first (high precedence unary), then and, then or. Without the parentheses, you might have expected different grouping. B::Deparse does not lie.

Part 6: SEEING DEFAULT VARIABLES

Perl's $_ is everywhere, and B::Deparse makes every implicit use visible:
$ perl -MO=Deparse -e 'for (@a) { chomp; s~foo~bar~; print }'
foreach $_ (@a) { chomp $_; s/foo/bar/; print $_; } -e syntax OK
Notice that chomp and print got explicit $_ arguments, but the substitution did not. That is because s~~~ always operates on $_ by default and B::Deparse does not bother making that one explicit. It is already in the canonical form.

This is useful for learning which builtins default to $_ and which do not. If B::Deparse adds $_, the builtin uses it as a default.

Part 7: UNDERSTANDING ONE-LINERS

Perl one-liners pack a lot of implicit behavior into very little syntax. B::Deparse unpacks them.
$ perl -MO=Deparse -ne 'print if /error/i'
LINE: while (defined($_ = <ARGV>)) { print $_ if /error/i; } -e syntax OK
The -n flag becomes a while loop. The diamond operator reads from ARGV. The implicit $_ appears. The LINE label shows up. All the hidden machinery is visible.
$ perl -MO=Deparse -ape 's~\t~,~g'
LINE: while (defined($_ = <ARGV>)) { s/\t/,/g; } continue { die "-p destination: $!\n" unless print $_; } -e syntax OK
The -p flag adds a continue block that prints $_ after every iteration. And it wraps the print in a die for error checking. That is a lot of hidden behavior from a single flag.

Part 8: DEBUGGING WEIRD SYNTAX

Sometimes you write something and are not sure if Perl parsed it the way you intended. B::Deparse settles the question.
$ perl -MO=Deparse -e 'print (1 + 2) * 3'
print(3); '???' * 3; -e syntax OK
Surprise. Perl parsed print(1 + 2) as a function call, then tried to multiply the return value by 3. The (1 + 2) was treated as the argument list to print, not as grouping parentheses. The * 3 became a separate, useless statement.

The ??? is B::Deparse's way of saying "this value is the return of print, but I cannot represent it cleanly." The code is technically valid but almost certainly not what you intended.

This is why print +(1 + 2) * 3 or print((1 + 2) * 3) exist. B::Deparse would have shown you the problem before you spent an hour debugging wrong output.

Part 9: THE ROUND-TRIP TEST

B::Deparse is useful for testing your understanding of Perl syntax. Write something, deparse it, and see if the result matches your expectations. If it does not, you learned something.
$ perl -MO=Deparse -e 'my $x = 5 if 0'
'???' if 0; -e syntax OK
That my $x = 5 if 0 trick (sometimes used for static variables in old code) deparses into something that makes it clear Perl treats the my declaration as happening but the assignment as conditional. The ??? shows B::Deparse cannot cleanly represent this edge case.

Another round-trip:

$ perl -MO=Deparse -e 'use 5.010; say for sort reverse @a'
use feature 'say'; say $_ foreach (sort reverse @a); -e syntax OK
Perl expanded use 5.010 into use feature 'say'. The chain of list operations is preserved. The postfix for became foreach. All normalizations that show you the canonical interpretation.

Part 10: A TOOL FOR UNDERSTANDING

.--. |o_o | "I wrote one thing. |:_/ | Perl compiled another. // \ \ B::Deparse showed me (| | ) the truth." /'\_ _/`\ \___)=(___/
B::Deparse is not a tool you use every day. It is a tool you use when Perl surprises you. When your code does something unexpected. When someone else's code looks like nonsense. When you are not sure how an expression is parsed.

It shows you the canonical form. The way Perl understood your code after parsing but before execution. No ambiguity. No implicit defaults. No syntactic sugar. Just the raw, normalized Perl that the interpreter actually compiled.

$ perl -MO=Deparse your_script.pl > deparsed.pl
The output is valid Perl. You can run it. You can diff it against your original. You can use it to understand code you inherited from someone who thought clever was a compliment.

B::Deparse ships with Perl. No installation. No CPAN. It has been there since Perl 5.005. Twenty-plus years of being the answer to "what does Perl think my code means?"

Ask it. It will tell you the truth.

perl.gg