Skip to content
VLSI Mentor

Perl Part I · Module 14 · Errors, Warnings, and Debugging

Decoding the Classic Warnings

Perl has a long list of warnings and you will meet four of them constantly. Each is worth learning properly, because each is a precise statement about your data rather than a general complaint — once you can read them, the warning usually tells you which line of which input file is wrong. The half that gets left out of most explanations is what Perl did next, and that is the half that matters: every one of these warnings is followed by Perl carrying on with a value it invented. This chapter measures the exact text of each warning and the exact value it produced, including one case that turns a timing number with a unit attached into a confident wrong answer.

Intermediate18 min readPerlPart IDebugging

Part I · Module 14 · Chapter 14.7 · Decoding the Classic Warnings

1. The Four, With Their Exact Text

To compare the messages reliably they need to be on standard output, so this script installs a handler that catches each warning as it happens. That is $SIG{__WARN__}, which Chapter 14.11 uses properly; here it is just a way to collect the text.

Azvya Education Pvt. Ltd.VLSI Mentor
the-four-warnings.pl
use strict;
use warnings;

my @seen;
local $SIG{__WARN__} = sub {
    my $w = shift;
    $w =~ s/ at .* line \d+\.?\s*\z//;    # drop the file and line: not stable
    $w =~ s/\s+\z//;
    push @seen, $w;
};

# 1. a value that was never set
my %slack = (A => 0.44);
my $sum = $slack{MISSING} + 1;

# 2. a string used as a number -- twice, two different operators
my $with_unit = "3.5ns" + 1;
my $nonsense  = "abc" * 2;

# 3. a list with a missing pair
my @pairs = ('A', 0.44, 'B');
my %h = @pairs;

# 4. a subroutine that calls itself too deeply
sub countdown { my $d = shift; return $d if $d > 120; return countdown($d + 1) }
countdown(1);

printf "%d. %s\n", $_ + 1, $seen[$_] for 0 .. $#seen;
printf "\n%d warnings\n", scalar @seen;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
1. Use of uninitialized value $slack{"MISSING"} in addition (+)
2. Argument "3.5ns" isn't numeric in addition (+)
3. Argument "abc" isn't numeric in multiplication (*)
4. Odd number of elements in hash assignment
5. Deep recursion on subroutine "main::countdown"

5 warnings

That is five messages for four kinds — the non-numeric warning appears twice because the script triggers it with two different operators, and Perl names the operator each time.

Notice how specific they are. Not "bad value" but $slack{"MISSING"} — the variable and the key. Not "type error" but Argument "3.5ns" isn't numeric in addition (+) — the offending string and the operator. Not "recursion problem" but main::countdown — the subroutine by name.

That specificity is the reason to read the whole message rather than skim it. Most of the time it identifies the problem completely.

2. The Half That Is Usually Left Out

Every one of those warnings was followed by Perl computing something anyway.

Azvya Education Pvt. Ltd.VLSI Mentor
what-it-computed.pl
use strict;
use warnings;
no warnings;                      # quietened so the RESULTS are visible

my %slack = (A => 0.44);

printf "undef + 1            = %s\n", $slack{MISSING} + 1;
printf "'3.5ns' + 1          = %s\n", "3.5ns" + 1;
printf "'0.44ns' * 2         = %s\n", "0.44ns" * 2;
printf "'abc' * 2            = %s\n", "abc" * 2;
printf "'1e-9' + 0           = %s\n", "1e-9" + 0;

my %h = ('A', 0.44, 'B');
printf "odd list -> B is     %s\n", defined $h{B} ? "'$h{B}'" : "undef";
printf "odd list -> %d keys: %s\n", scalar(keys %h), join(",", sort keys %h);
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
undef + 1            = 1
'3.5ns' + 1          = 4.5
'0.44ns' * 2         = 0.88
'abc' * 2            = 0
'1e-9' + 0           = 1e-09
odd list -> B is     undef
odd list -> 2 keys: A,B

Read the second line again. '3.5ns' + 1 is 4.5.

Perl parsed the leading 3.5, discarded the ns, and did the arithmetic. It did not fail. It did not give you zero, which you might have spotted. It gave you a plausible number that is wrong only because the units were never checked — and in a track where every timing report has units on every value, this is the warning to take seriously.

The other rows:

  • undef + 1 is 1. The missing value became zero.
  • 'abc' * 2 is 0. No leading digits at all, so the whole string is zero.
  • '1e-9' + 0 is 1e-09 — and this one is correct, with no warning. Perl understands scientific notation, which is worth knowing because it means you cannot tell a parseable string from an unparseable one by eye.
  • The odd list left B mapped to undef, and the hash still has two keys. Nothing was dropped; the last key simply got nothing.

3. Each One, And What To Do About It

Use of uninitialized value

What it means. You read a variable, hash key or array element that was never given a value, and used it in an operation that needs one.

What Perl did. Treated it as 0 in arithmetic, or "" in string context.

Where it comes from. Usually a hash key that is not there — a pin name missing from a report, a field missing from a line that had fewer columns than you expected.

The fix is to decide what a missing value means, rather than letting Perl decide:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# refuse it
die "no slack recorded for $pin\n" unless defined $slack{$pin};

# or default it, deliberately and visibly
my $value = $slack{$pin} // 0;

# or skip it
next unless defined $slack{$pin};

The // operator from Chapter 3.8 is the right tool here precisely because it tests definedness and not truth, so a real 0 survives it.

Argument isn't numeric

What it means. A string was used where a number was needed, and it did not look entirely like a number.

What Perl did. Took any leading numeric part and ignored the rest — measured, "3.5ns" became 3.5.

The fix is to strip units deliberately, so you can tell the difference between a value with a unit and a value that is genuinely broken:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# pull out the number AND notice if there is nothing to pull
if ($field =~ /\A \s* (-?\d+(?:\.\d+)?) \s* (ps|ns|us)? \s* \z/x) {
    my ($number, $unit) = ($1, $2);
    ...
}
else {
    warn "line $.: cannot read '$field' as a slack value\n";
    next;
}

Chapter 11.8's anchors are doing the real work there: \A and \z mean the whole field must match, so 3.5ns junk is rejected rather than quietly accepted.

Odd number of elements in hash assignment

What it means. You built a hash from a list with an unpaired final element.

What Perl did. Gave the last key an undefined value — measured, the hash still had two keys and B was undef.

Where it comes from. Almost always a split on a line that was shorter than expected, or a function returning one fewer value than you assumed.

The fix is to check the count before you build the hash:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
my @fields = split /\s+/, $line;
unless (@fields % 2 == 0) {
    warn "line $.: ", scalar @fields, " fields, expected an even number\n";
    next;
}
my %row = @fields;

This is also a warning that often fires on the last line of a file, where a trailing newline or a blank line produced a short list. Chapter 13.2's chomp discipline prevents a fair share of them.

4. Deep Recursion

The fourth is different in kind from the other three, so it is worth its own section.

What it means. A subroutine has called itself about a hundred times without returning. Perl warns once, early, and then keeps going.

What Perl did. Carried on recursing. The warning is not a limit; it is a remark.

Azvya Education Pvt. Ltd.VLSI Mentor
deep-recursion.pl
use strict;
use warnings;

my $current = 0;
my @fired_at;
local $SIG{__WARN__} = sub { push @fired_at, $current };

my $deepest = 0;
sub descend {
    my ($depth) = @_;
    $current = $depth;
    $deepest = $depth if $depth > $deepest;
    return $depth if $depth >= 500;
    return descend($depth + 1);
}

my $result = descend(1);
printf "returned %d, reached depth %d\n", $result, $deepest;
printf "warnings issued: %d, at call depth: %s\n",
    scalar @fired_at, join(",", @fired_at);
print "it warned once, early, then recursed 400 more times anyway\n";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
returned 500, reached depth 500
warnings issued: 1, at call depth: 99
it warned once, early, then recursed 400 more times anyway

It warned exactly once, at the depth shown, and then carried on for another four hundred calls. So the warning is a one-time hint that something may be wrong, not a guard against anything — and if you are scanning a long log for repeated complaints, there is only ever one to find.

There are two quite different causes, and they need opposite responses:

A genuine bug — recursion with no exit. A directory walker that follows a symbolic link back to a parent, or a function whose base case is never reached. Here the warning is a real bug report and the recursion would run until memory ran out. Chapter 13.10's File::Find discussion covers the symbolic-link case specifically.

Legitimately deep work. Walking a deep hierarchy, or traversing a netlist that really is 300 levels down. Here nothing is wrong and the warning is noise. Switch it off for that subroutine only:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sub walk_netlist {
    no warnings 'recursion';     # deliberate: depth is data-driven here
    ...
}

Keep the no warnings inside the recursive subroutine and name the category, so you have switched off the one remark you have decided about and nothing else — the 14.6 discipline.

5. All Four At Once: A Parser That Does Not Guess

Each warning on its own is easy to fix. The useful thing is a reading loop that cannot produce any of them, because it checks the shape of every line before it does arithmetic. This is the pattern to reuse.

Azvya Education Pvt. Ltd.VLSI Mentor
defensive-parse.pl
use strict;
use warnings;

# one good line, then one of each problem from this chapter
my @lines = (
    "U1/Q      0.44ns   setup",
    "U2/D      3.5      setup",
    "U3/CK     --       setup",
    "U4/Q      0.12ns",
    "U5/Q      abc      setup",
);

my ($good, $skipped) = (0, 0);

for my $i (0 .. $#lines) {
    my $lineno = $i + 1;
    my @f = split /\s+/, $lines[$i];

    # guard 1: the right number of fields, BEFORE unpacking them
    if (@f != 3) {
        printf "line %d: SKIP  expected 3 fields, got %d\n", $lineno, scalar @f;
        $skipped++;
        next;
    }
    my ($pin, $raw, $check) = @f;

    # guard 2: the whole field is a number, with an optional unit
    unless ($raw =~ /\A(-?\d+(?:\.\d+)?)(ps|ns|us)?\z/) {
        printf "line %d: SKIP  '%s' is not a slack value\n", $lineno, $raw;
        $skipped++;
        next;
    }
    my ($number, $unit) = ($1, $2 // "(none)");

    printf "line %d: OK    %-8s %7.3f unit=%s\n", $lineno, $pin, $number, $unit;
    $good++;
}

printf "\n%d usable, %d skipped of %d lines\n", $good, $skipped, scalar @lines;

# and the 14.2 discipline: act on the COUNT, do not just report each row
die "too many bad rows, refusing to report\n" if $skipped / @lines > 0.5;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
line 1: OK    U1/Q       0.440 unit=ns
line 2: OK    U2/D       3.500 unit=(none)
line 3: SKIP  '--' is not a slack value
line 4: SKIP  expected 3 fields, got 2
line 5: SKIP  'abc' is not a slack value

2 usable, 3 skipped of 5 lines

Not one warning was produced, because nothing was ever used as a number without being checked first. Four things are doing the work:

The field count is checked before unpacking. Line 4 had two fields. Had the script assigned straight into three variables, $check would have been undef and the odd-elements or uninitialized warning would have followed later, far from the cause.

The numeric field is matched whole. \A and \z mean the entire field must be a number with an optional unit, so -- and abc are rejected outright rather than silently becoming 0. Line 1's 0.44ns is accepted and its unit is captured, instead of being quietly discarded as section 2 measured.

A missing unit is handled explicitly. Line 2 has a bare 3.5, and $2 // "(none)" records that rather than leaving undef to warn later. Note // and not || — Chapter 3.8's distinction, which matters here because a captured 0 is a real value.

Every rejection names the line and the reason. warn plus next over die, so one pass reports all three bad rows, exactly as Chapter 14.2 argued.

Then the last line is the part most scripts leave out: three of five rows were unusable, so the script refuses to report at all. Without that check it would have printed a confident summary built from 40% of the data and exited 0 — Chapter 14.1's third kind of failure, which is the one nothing complains about.

6. What To Remember

  • The messages are specific. Measured, they name the hash key, the offending string, the operator and the subroutine.
  • Every one of these warnings is followed by Perl continuing with a value it chose.
  • undef becomes 0 in arithmetic and "" in string context.
  • Measured: '3.5ns' + 1 is 4.5. Perl takes the leading number and discards the unit — a plausible wrong answer, not an obvious one.
  • Measured: 'abc' * 2 is 0, and '1e-9' + 0 is correct with no warning, so you cannot judge parseability by eye.
  • For a missing value, decide: die, // 0, or next — and use // so a real zero survives.
  • For a non-numeric value, match the whole field with \A and \z and pull the unit out on purpose.
  • An odd list leaves the last key undef and keeps the other pairs. Check @fields % 2 before building the hash.
  • Deep recursion warns once, early, and carries on. Measured: one warning, then 400 further calls to depth 500.
  • Two causes, opposite fixes — a missing base case is a real bug; genuinely deep data wants no warnings 'recursion'; inside that subroutine only.
  • A depth guard beats silence for the bug case, because it names where it happened.
  • Guard the shape of the line, not the individual values. Measured: checking the field count and matching the numeric field whole produced no warnings at all from input containing every problem in this chapter.
  • Capture the unit instead of discarding it, and use // so a real 0 survives.
  • Act on the count of bad rows. Three of five unusable meant the script refused to report, rather than exiting 0 with a confident summary built from 40% of the data.

Chapter 14.8 turns to the oldest debugging tool there is. Printing a value to find out what it holds is perfectly respectable; doing it so the output is actually readable, goes to the right stream, and can be switched off without deleting it, takes a little more than a bare print.

Where this fits

Part of the Perl curriculum.