NAME
Switch::Declare - compile-time, lexically-scoped switch/case
SYNOPSIS
use Switch::Declare;
# statement form
switch ($value) {
case 200 { handle_ok() } # numeric -> ==
case "GET" { handle_get() } # string -> eq
case /^\d+$/ { all_digits() } # regex -> =~
case [400 .. 499] { client_error() } # range -> >= && <=
case ["a","b","c"] { in_set() } # list -> membership
case \&is_weekend { weekend() } # predicate -> $code->($topic)
case ref(ARRAY) { handle_aref() } # reference type (exact)
case reftype(HASH) { handle_href() } # underlying type (through blessing)
case isa("Plan") { handle_plan() } # blessed object derived from class
case MAX_RETRIES { exhausted() } # named constant (use constant)
case == $limit { at_limit() } # numeric compare vs a variable
case eq $name { named() } # string compare vs a variable
case =~ $pattern { matched() } # regex match vs a runtime pattern
case undef { missing() } # the topic is undef
default { fallback() }
}
# expression form - yields the matched arm's value
my $label = switch ($status) {
case 200 { "ok" }
case 404 { "missing" }
default { "other" }
};
DESCRIPTION
Switch::Declare installs a switch keyword that parses an entire switch (EXPR) { ... } construct at compile time and lowers it to an ordinary Perl optree. All of the parsing work happens once, at compile time - nothing of the parser remains at runtime.
The construct is a real lexical pragma: the switch keyword is recognised only within the lexical scope of a use Switch::Declare (and can be switched off again with no Switch::Declare). Outside such a scope switch is an ordinary identifier, so the keyword never collides with unrelated code.
The scrutinee is evaluated exactly once. The first matching case wins; there is no implicit fallthrough. A trailing default matches when no case did. Used as an expression the construct yields the value of the executed block (undef if nothing matched and there is no default); used as a statement it simply runs the matched block.
Pattern kinds
Each case pattern is recognised at compile time and lowered to the cheapest matching op:
a number literal compiles to numeric
==a string literal (
'...'or"...") compiles to stringeqa /.../ regex (with optional
imsxflags) matches the topica
[ LO .. HI ]range compiles to an inclusive bounds test (numeric or string depending on the bound literals)a
[ a, b, c ]list compiles to a membership test (anORchain of equality tests, each numeric or string per element)a predicate - either
\&name(optionally package-qualified,\&Pkg::name) or an inlinesub { ... }- is called with the topic as its argument; a true return matches. An inline sub closes over the enclosing lexicals:my $limit = 100; switch ($n) { case sub { $_[0] > $limit } { "over" } default { "ok" } }ref(TYPE)matches whenref($topic) eq "TYPE"- the usualARRAY/HASH/CODE/SCALAR/Regexp/... names, or a class name for exact-class dispatch (ref($obj) eq "My::Class"). Barerefmatches any reference.TYPEmay be a bareword or a quoted string.reftype(TYPE)is likeref(TYPE)but reports the underlying reference type, seeing through blessing - so a blessed arrayref matchesreftype(ARRAY)(whereref(ARRAY)would not). Barereftypematches any reference.isa(Class)matches a blessed object derived fromClass(a fast@ISAcheck). It does not match plain class-name strings or unblessed references, and never dies on a non-object. It is a direct@ISAwalk, so it does not invoke an overriddenisa()/DOESmethod.undefmatches when the topic is undefined (see "Undef and type safety").a named constant - a bareword naming an inlinable
use constant(optionally package-qualified,Pkg::FOO) - is folded to its value at compile time and classified exactly like the literal it holds: a numeric constant compiles to==, a string constant toeq(and is dispatch-eligible). It costs nothing at runtime -case FOOis byte-for-bytecase 1.a variable comparison
== $xoreq $xcompares the topic against a runtime scalar. Because a variable's type is unknown at compile time, the comparison operator is written out:== $xis numeric (==,looks_like_number-guarded on both sides),eq $xis string (eq) - exactly as in Perl itself. The operand is a plain scalar ($nameor$Pkg::name); both are undef-safe (an undef topic or an undef/non-numeric variable simply does not match, and never warns):my $limit = 100; my $tag = "draft"; switch ($value) { case == $limit { "at the limit" } case eq $tag { "a draft" } default { "other" } }a runtime regex match
=~ $rxmatches the topic against a pattern held in a scalar - aqr//object (recommended) or a string used as a pattern. This complements the compile-time/literal/form for the case where the pattern is only known at run time. It is undef-safe (an undef topic or undef pattern simply does not match, without warning). Being a runtime match outside a realm//op, it is a pure membership test and does not set the capture variables ($1,@+); if you need captures from a variable pattern, use a predicate arm,case sub { $_[0] =~ $rx }. The operand is a plain scalar ($nameor$Pkg::name):my $rx = qr/^\d{4}-\d{2}-\d{2}$/; switch ($field) { case =~ $rx { "looks like a date" } default { "something else" } }
Undef and type safety
Every pattern is undef and type-safe: a topic never produces a warning, and only matches a pattern the comparison is actually meaningful for.
An undef topic matches only an explicit
case undef; otherwise it falls through todefault. It never warns, and it never accidentally matchescase 0orcase "".A numeric pattern (number literal, range, or numeric list element) matches only a topic that
looks_like_number. A non-numeric topic neither matches nor warns - soswitch("one") { case 1 {...} }is silent (and, unlike a hand-written"one" == 0, never mis-matchescase 0).
This is a deliberate difference from a naive hand-written ==/eq chain, which would warn (Use of uninitialized value, Argument isn't numeric) on the same inputs. Switch::Declare behaves like a correctly guarded chain.
Patterns are deliberately a small, predictable grammar rather than arbitrary expressions, so classification is unambiguous and the emitted code is as tight as a hand-written conditional.
PERFORMANCE
The keyword plugin emits the op tree directly in place of the keyword, so there is no wrapper subroutine call per evaluation. The chain of case tests lowers to a native conditional expression - the very same ==/eq/=~/bounds ops you would write by hand.
For a string, regex, predicate, or reference switch over a plain variable or constant scrutinee with single-expression arms, the construct compiles to exactly a hand-written if/elsif (ternary) chain: no temporary, no enclosing scope, no extra ops. In the bundled benchmark (xt/bench.pl) these run within measurement noise of a hand-rolled chain (0-2%).
A numeric switch pays for its type safety (see "Undef and type safety"): each numeric comparison is guarded by looks_like_number, computed once per evaluation and shared across the arms. It therefore runs on par with an equivalently guarded hand-written chain (within ~3% in xt/bench.pl), and roughly 40-45% slower than a naive unguarded $x == N chain - which is the cost of not warning or mis-matching on non-numeric input. If you know the topic is always numeric and want the last drop of speed, a string switch or a hand-rolled chain avoids the guard.
Dispatch mode
When a switch is effectively a lookup table - every case maps a string literal to a constant value, with at least a handful of arms - it is lowered to a single O(1) hash lookup against a hash built once at compile time, instead of an O(n) chain of eq tests:
# compiles to a single hash lookup, not 6 string comparisons
my $name = switch ($code) {
case "GET" { "read" }
case "PUT" { "update" }
case "POST" { "create" }
case "DELETE" { "remove" }
case "PATCH" { "modify" }
case "HEAD" { "peek" }
default { "?" }
};
In the bundled benchmark a 20-arm string switch in dispatch mode runs about 2.5x faster than the equivalent hand-written if/elsif chain. The table is constructed once at compile time (not per call), so there is no per-evaluation build cost. Dispatch mode is chosen automatically; it never changes behaviour (numeric switches keep == semantics and stay as a chain; any non-constant arm or duplicate key falls back to the chain), so you never opt in or out.
AUTHOR
LNATION <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)