ADR-005: DRY in XS via XSUB ALIAS

Status

Accepted

Context

Params::Util's XS layer historically implemented each function as a standalone XSUB, even when functions share nearly identical logic. For example, _INSTANCE calls ->isa while _INSTANCEDOES calls ->DOES and _INSTANCECAN calls ->can -- the surrounding code (magic handling, blessed check, stack manipulation, return convention) is identical.

Duplicating 40+ lines of XS per variant violates DRY and makes maintenance error-prone. The XSUB ALIAS mechanism allows multiple Perl-visible function names to share one C entry point, distinguished only by the ix variable.

A concern raised during review of List::MoreUtils and Scalar::List::Utils is that ALIAS forces conditionals into a shared code path. For functions that loop over lists (e.g. uniq/uniqstr/ uniqint), a branch per iteration can hurt performance. However, Params::Util predicates perform a single check per call -- the dominant cost is always the Perl method dispatch (call_method), and an array-index lookup for the method name is negligible in comparison.

Decision

Consolidate the XS implementation into ALIAS groups where the only difference is the method name passed to call_method or the validation logic selected by ix:

Group 1: String validators

_IDENTIFIER and _CLASS -- differ only in the pattern matched against the string (identifier vs. class name with optional :: separators).

Group 2: Numeric validators

_POSINT and _NONNEGINT -- differ only in whether zero is accepted.

Group 3: Class dispatchers

_CLASSISA, _CLASSDOES, _CLASSCAN, _SUBCLASS -- class name validation followed by call_method with method name from a static table. _SUBCLASS adds one inequality check.

Group 4: Instance dispatchers

_INSTANCE, _INSTANCEDOES, _INSTANCECAN -- sv_isobject followed by call_method with method name from a static table.

Group 5: Invocant dispatchers

_INVOCANT, _INVOCANTCAN -- blessed-or-class check, optionally followed by call_method("can").

Functions deliberately excluded from XS acceleration:

_SET / _SET0

These loop over an array calling _INSTANCE per element. The per-element _INSTANCE is already XS-accelerated; the loop overhead in Perl is minimal. Callers needing high-performance list filtering should use "any" in List::Util or "all" in List::MoreUtils instead.

_HANDLE

Too many special cases (globs, tied handles, multiple isa checks). Rarely performance-critical.

_DRIVER

Dominated by require cost; XS around the dispatch adds no value.

Consequences

  • Each ALIAS group is maintained as a single block of C; bug fixes and improvements apply to all aliased functions simultaneously.

  • Adding a new variant (e.g. a future _CLASSDOES_ALL) requires only a new ALIAS line and a table entry, not duplicating the entire XSUB.

  • The existing _INSTANCE XSUB is refactored into the Group 4 ALIAS block, which is a non-functional change -- existing tests cover the behaviour.

  • No measurable performance regression from ALIAS dispatch: one static array index per call vs. hundreds of cycles for call_method.