Type Checking Cookbook

Real Perl patterns that PSC catches, with explanations and fixes. Each recipe shows the code, the diagnostic, and how to resolve it. For background on why PSC works the way it does, see the Type Checking guide.

Arity Mismatches

PSC knows how many arguments each built-in function expects. Pass too few or too many and it flags the call.

Missing arguments

push();                    # forgot the arguments entirely
splice($x);               # splice needs at least 2 args

Diagnostic:

file.pl:1:1: error: expected at least 2 arguments for push, got 0 [arity-mismatch]
file.pl:2:1: error: expected at least 2 arguments for splice, got 1 [arity-mismatch]

Fix: Supply the required arguments. push needs an array and at least one value. splice needs an array and an offset.

push(@items, $value);
splice(@items, 0);

Too many arguments

chr("hello", 2);           # chr takes exactly 1 argument

Diagnostic:

file.pl:1:1: error: expected 1 argument for chr, got 2 [arity-mismatch]

Fix: Remove the extra argument.

chr(72);                   # returns "H"

Container Type Mismatches

Many built-in functions require a specific container type. Passing a scalar where an array or hash is expected produces a type mismatch.

Scalar where Array expected

my $x = 1;
push($x, 1);              # push needs an array, not a scalar

Diagnostic:

file.pl:2:1: error: argument 1 of push expects Array, got Scalar [type-mismatch]

Why: The sigil $ declares $x as Scalar. push requires Array as its first argument. Even if $x holds an array reference at runtime, the container type is Scalar.

Fix: Use an array variable, or dereference:

my @items;
push(@items, 1);           # correct: @items is Array

# or, with a reference:
my $ref = [];
push(@$ref, 1);            # dereference to get Array

Scalar where Hash expected

my $x = 1;
keys($x);                 # keys needs a hash or array

Diagnostic:

file.pl:2:1: error: argument 1 of keys expects Hash, got Scalar [type-mismatch]

Fix: Use a hash variable:

my %lookup = (foo => 1, bar => 2);
keys(%lookup);             # correct

Hash where Array expected

my %hash = (a => 1);
push(%hash, 1);            # push needs an array, got a hash

Diagnostic:

file.pl:2:1: error: argument 1 of push expects Array, got Hash [type-mismatch]

Fix: Use the correct container. If you meant to add a key-value pair to the hash:

$hash{b} = 2;              # assign directly

Value Type Mismatches

PSC tracks the specific type of a variable through assignment. When you assign 42 to $n, PSC narrows its type from Scalar to Int. Functions that expect Hash or Array then reject it.

Int where Hash expected

my $n = 42;
keys($n);                  # keys needs Hash, $n is Int

Diagnostic:

file.pl:2:1: error: argument 1 of keys expects Hash, got Int [type-mismatch]

Why: The assignment $n = 42 narrows $n from Scalar to Int. Int is more specific than Scalar — PSC uses the most precise type available. An Int is not a Hash.

Int where Array expected

my $count = 42;
splice($count);            # splice needs Array, $count is Int

Diagnostic:

file.pl:2:1: error: argument 1 of splice expects Array, got Int [type-mismatch]

Num where Hash expected

my $f = 3.14;
values($f);                # values needs Hash, $f is Num

Diagnostic:

file.pl:2:1: error: argument 1 of values expects Hash, got Num [type-mismatch]

Coercion Mismatches

Perl silently coerces values across types. Adding a string to a number produces 0. Concatenating an array produces a count. These coercions are legal Perl but usually wrong. PSC flags them.

List in arithmetic context

my %h;
my $a = keys(%h) + 1;     # keys returns List, + expects Num

Diagnostic:

file.pl:2:10: warning: left operand of + expects Num, got List [coercion-mismatch]

Why: keys(%h) returns a List. The + operator expects Num operands. Perl coerces the list to its count, which works — but the intent is ambiguous. Did you mean the count?

Fix: Be explicit about the count:

my $a = scalar(keys(%h)) + 1;  # explicit count

Array in string context

my @arr;
my $b = @arr . 1;         # . expects Str, @arr is Array

Diagnostic:

file.pl:2:10: warning: left operand of . expects Str, got Array [coercion-mismatch]

Why: The . concatenation operator expects strings. An array in string context becomes its count, so @arr . 1 produces something like "01". This is rarely what you want.

Fix: Join the array or use its count explicitly:

my $b = join(",", @arr) . 1;   # concatenate elements
my $b = scalar(@arr) . 1;      # concatenate count

Undef Propagation

An uninitialized variable has type Scalar, which includes Undef. Using it in arithmetic is a common source of bugs.

Uninitialized variable in arithmetic

my $x;
my $y = $x + 1;           # $x may be undef

Diagnostic:

file.pl:2:10: warning: left operand of + expects Num, got Scalar [coercion-mismatch]

Why: $x is declared but never assigned, so its type is Scalar (the broadest scalar type, which includes Undef). The + operator expects Num. Perl coerces undef to 0, but this is usually a bug — you forgot to initialize the variable.

Fix: Initialize the variable, or add a defined guard:

my $x = 0;                 # initialize
my $y = $x + 1;           # $x is now Int, no warning

# or guard against undef:
my $x;
if (defined($x)) {
    my $y = $x + 1;        # inside guard, $x is non-Undef
}

Guard Patterns

PSC narrows variable types inside guard blocks. A guard is a conditional that tests a variable's type at runtime. Inside the block where the guard holds, PSC uses the narrower type.

defined — remove Undef

my $name;                  # Scalar (may be Undef)

if (defined($name)) {
    print length($name);   # OK: $name is Scalar without Undef
}

print length($name);       # warning: Scalar includes Undef

The defined guard removes Undef from the type. Outside the block, the original type applies.

ref — narrow to reference type

my $data;                  # Scalar

if (ref($data)) {
    # $data is now Ref — a reference of some kind
}

if (ref($data) eq 'HASH') {
    keys($data);           # OK: $data is HashRef
}

if (ref($data) eq 'ARRAY') {
    push(@$data, 1);       # OK: $data is ArrayRef
}

A bare ref($data) narrows to Ref. Comparing ref($data) eq 'TYPE' narrows to the specific reference type.

isa — narrow to Object

my $obj;                   # Scalar

if ($obj isa Some::Class) {
    # $obj is now Object
    $obj->method();        # OK
}

Negated guards

my $val;                   # Scalar

unless (defined($val)) {
    # $val is Undef here
    return;
}

# After the unless block, $val is non-Undef
print length($val);        # OK

Negated guards (via unless or else blocks) apply the inverse narrowing.

Clean Code

PSC produces no output for correct code. Here is a file that passes all checks:

use strict;
use warnings;

my @numbers = (1, 2, 3);
my $count = push(@numbers, 4);     # push returns Int (count)

my %lookup = (foo => 1, bar => 2);
my @keys = keys(%lookup);          # keys returns List
my $exists = exists($lookup{foo}); # exists returns Bool
$ psc check clean.pl
$ echo $?
0

No output and exit code 0 means no type errors.

The Type Hierarchy

PSC uses this type hierarchy, derived from how Perl values behave:

Any
 ├── Scalar
 │    ├── Str
 │    │    ├── Num
 │    │    │    └── Int
 │    │    └── Bool
 │    ├── Undef
 │    ├── DualVar
 │    ├── NaN
 │    ├── Inf
 │    ├── Regex
 │    └── Ref
 │         ├── ScalarRef
 │         ├── ArrayRef
 │         ├── HashRef
 │         ├── CodeRef
 │         ├── GlobRef
 │         └── Object
 ├── Array
 ├── Hash
 ├── Code
 └── Glob

Arrows point from subtype to supertype. Int is a subtype of Num because every integer is a valid number. Num is a subtype of Str because every number can be represented as a string. This mirrors Perl's coercion chain.

PSC infers the most specific type it can. A literal 42 is Int, not Scalar. A variable my $x with no assignment is Scalar — the broadest scalar type. The more PSC knows, the more it can check.

Where Types Come From

PSC infers types from five sources. You write no annotations — the types are already in the code.

SourceExampleInferred Type
Sigilmy $xScalar
Sigilmy @xsArray
Sigilmy %hHash
Literal42Int
Literal3.14Num
Literal"hello"Str
Literal/pattern/Regex
LiteralundefUndef
Operator$a + $bNum
Operator$a . $bStr
Operator$a == $bBool
Builtinpush(@a, $v)Int (count)
Builtinkeys(%h)List
Builtindefined($x)Bool
Assignmentmy $n = 42Int (narrowed from Scalar)
Guardif (defined($x))non-Undef inside block

Diagnostic Codes

CodeSeverityMeaning
arity-mismatcherrorWrong number of arguments to a built-in function
type-mismatcherrorArgument type incompatible with what the function expects
coercion-mismatchwarningOperator receives a type that Perl coerces silently — likely a bug
unknown-builtinwarningPSC does not recognize the function name

Every diagnostic line follows this format:

filename:line:col: severity: message [code]
  hint: suggestion (when available)

Hints appear only when PSC can suggest a guard that resolves the mismatch.

Further Reading