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.
| Source | Example | Inferred Type |
|---|---|---|
| Sigil | my $x | Scalar |
| Sigil | my @xs | Array |
| Sigil | my %h | Hash |
| Literal | 42 | Int |
| Literal | 3.14 | Num |
| Literal | "hello" | Str |
| Literal | /pattern/ | Regex |
| Literal | undef | Undef |
| Operator | $a + $b | Num |
| Operator | $a . $b | Str |
| Operator | $a == $b | Bool |
| Builtin | push(@a, $v) | Int (count) |
| Builtin | keys(%h) | List |
| Builtin | defined($x) | Bool |
| Assignment | my $n = 42 | Int (narrowed from Scalar) |
| Guard | if (defined($x)) | non-Undef inside block |
Diagnostic Codes
| Code | Severity | Meaning |
|---|---|---|
arity-mismatch | error | Wrong number of arguments to a built-in function |
type-mismatch | error | Argument type incompatible with what the function expects |
coercion-mismatch | warning | Operator receives a type that Perl coerces silently — likely a bug |
unknown-builtin | warning | PSC 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
- Type Checking Guide — philosophy and architecture
- PSC Reference — command-line usage
- Practical Type System Paper — the type hierarchy in detail
- Formal Type System Paper — mathematical foundations