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 string eq

  • a /.../ regex (with optional imsx flags) matches the topic

  • a [ 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 (an OR chain of equality tests, each numeric or string per element)

  • a predicate - either \&name (optionally package-qualified, \&Pkg::name) or an inline sub { ... } - 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 when ref($topic) eq "TYPE" - the usual ARRAY/HASH/CODE/SCALAR/Regexp/... names, or a class name for exact-class dispatch (ref($obj) eq "My::Class"). Bare ref matches any reference. TYPE may be a bareword or a quoted string.

  • reftype(TYPE) is like ref(TYPE) but reports the underlying reference type, seeing through blessing - so a blessed arrayref matches reftype(ARRAY) (where ref(ARRAY) would not). Bare reftype matches any reference.

  • isa(Class) matches a blessed object derived from Class (a fast @ISA check). It does not match plain class-name strings or unblessed references, and never dies on a non-object. It is a direct @ISA walk, so it does not invoke an overridden isa()/DOES method.

  • undef matches 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 to eq (and is dispatch-eligible). It costs nothing at runtime - case FOO is byte-for-byte case 1.

  • a variable comparison == $x or eq $x compares the topic against a runtime scalar. Because a variable's type is unknown at compile time, the comparison operator is written out: == $x is numeric (==, looks_like_number -guarded on both sides), eq $x is string (eq) - exactly as in Perl itself. The operand is a plain scalar ($name or $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 =~ $rx matches the topic against a pattern held in a scalar - a qr// 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 real m// 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 ($name or $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 to default. It never warns, and it never accidentally matches case 0 or case "".

  • 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 - so switch("one") { case 1 {...} } is silent (and, unlike a hand-written "one" == 0, never mis-matches case 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)