NAME

PAX - Perl Adaptive eXecution compiler and standalone binary packager

VERSION

Current release version is kept in our $VERSION in this module and mirrored to every lib/PAX/*.pm module before release.

SYNOPSIS

perl bin/pax help
perl bin/pax build
perl bin/pax build -o ./build/my-app bin/my-app
perl bin/pax run -- version
perl bin/pax run bin/my-app -- version

DESCRIPTION

PAX turns a Perl entrypoint plus its repeatable build inputs into a standalone executable. The executable can include compiled code units, native artifacts where supported, asset payloads, dependency payloads, and a runtime launcher.

The project is deliberately neutral. Core compiler, packaging, loader, runtime, and dispatch code must not embed assumptions about one application, company, or module namespace.

INTRODUCTION

PAX exists to change the deployment shape of a Perl application.

Without PAX, a Perl application commonly depends on some mix of the original source tree, a host Perl installation, host CPAN modules, asset directories next to the app, and local bootstrap scripts or container images that carry the whole working tree.

PAX aims to turn that into one executable that can carry compiled code units, runtime payloads, embedded assets, and native artifacts where a region can be proven safe to specialize.

In bundled-runtime mode that includes the packaged helper programs and the linked shared libraries and SONAME aliases required by bundled XS modules, so helper commands and query/runtime helpers still work after the original source tree and CPAN installation are gone.

The goal is not to pretend every Perl feature can become a native binary with no trade-offs. The real goal is:

  • keep Perl correctness

  • keep fallback execution explicit

  • package applications into one binary

  • move eligible hot paths toward native speed

  • stay neutral across arbitrary Perl projects

WHAT YOU GET

  • one public command surface with pax build and pax run

  • a repeatable build contract through paxfile.yml

  • one standalone executable output

  • embedded asset packaging for web applications and static payloads

  • runtime payload packaging for source-tree-free execution

  • packaged helper commands plus linked XS shared libraries and SONAME aliases for source-tree-free execution

  • static standalone analysis for plain Perl scripts, so long-running entrypoints do not execute themselves during pax build

  • self-hosted build capability, including building bin/pax itself

  • Docker-friendly multi-stage packaging

MAIN CONCEPTS

Public Facade

PAX::CLI owns the public operator contract behind pax build and pax run.

Manifest Loading

PAX::Paxfile loads repeatable build inputs from paxfile.yml.

Standalone Image Builder

PAX::StandaloneImage collects dependencies, packages runtime payloads, embeds assets, and writes the standalone launcher.

Code Unit Compilation

PAX::CodeUnitCompiler lowers supported Perl source shapes into PAX code unit records. Unsupported regions remain on explicit fallback paths instead of being silently miscompiled.

Packaged Runtime

PAX::StandaloneRuntime provides the packaged helper runtime used after the standalone executable starts.

Native Dispatch

PAX::StandaloneDispatch and related runtime pieces execute packaged native regions and deopt fallback behavior under the standalone model.

SOW-03 PUBLIC COMMAND SURFACE

PAX exposes only two public commands through bin/pax:

  • build

    Compile and package the source tree behind an entrypoint into one standalone executable.

  • run

    Run the same build flow and then execute the resulting binary with arguments after --.

  • interpreter mode

    If pax is invoked with a plain Perl script path instead of build or run, it executes that script directly as interpreter-mode shebang execution.

The canonical usage is:

pax build ...
pax run ...

Interpreter-mode execution is intended for the case where a built pax binary is installed at a stable path and then used from a shebang line such as #!/usr/local/bin/pax. In that path, PAX::CLI treats the script argument as a direct Perl program to run under the packaged runtime instead of as a CLI subcommand.

Common CLI switches include:

  • --paxfile, --no-paxfile

  • -I, to prepend Perl library directories for inline builds/runs

  • -M, to load and import Perl modules for inline builds/runs

  • -e, to synthesize an entrypoint from inline Perl code

  • --lib, --source-root, --cpanfile, --asset, --asset-dir

  • --output / -o

  • --runtime-mode

  • --compact

Internal diagnostics and validation modules remain available as Perl APIs for the test suite and release gates. They are not public bin/pax subcommands.

PAXFILE CONTRACT

When no positional entrypoint is supplied, build and run read paxfile.yml by default. --paxfile selects a different manifest and --no-paxfile disables manifest loading. When a positional entrypoint is supplied on the CLI, PAX treats that target as an isolated build and does not silently inherit libs, source_roots, assets, asset_dirs, cpanfiles, or app metadata from the ambient paxfile.yml. An explicit --paxfile still applies its manifest defaults.

Supported manifest keys:

  • name

  • entrypoint

  • libs

  • source_roots

  • assets

  • asset_dirs

  • cpanfiles

  • output

  • runtime_mode

  • app_name, app_namespace, app_entrypoint_env, app_entrypoint_fallback, app_command

CLI flags override file values. Output path precedence is:

1. --output / -o
2. paxfile.yml output
3. .pax/standalone/<name/<name>>

MANUAL

Installation

For development from a repository checkout:

cpanm --installdeps .
perl bin/pax help

For release packaging:

cpanm Dist::Zilla

First Build

The simplest workflow is a local paxfile.yml:

name: example-app
entrypoint: bin/example-app
output: build/example-app
libs:
  - lib
cpanfiles:
  - cpanfile
runtime_mode: bundled_perl

Then build:

perl bin/pax build

Long builds print a DD-style task rundown on stderr by default. On a real terminal the board redraws live; in non-interactive runs it prints a static rundown while the machine-readable build payload stays on stdout. The build path is broken into concrete checkpoints such as code-unit compilation, application metadata inference, dependency analysis, native artifact analysis, manifest writing, and launcher compilation. The code-unit phase is further split into source discovery, entrypoint compilation, application unit compilation, and dependency unit compilation so long builds keep moving visibly. Application unit compilation includes the current file name, so a slow module no longer looks like a frozen counter. Set PAX_PROGRESS=0 to suppress the rundown.

And run the result directly:

./build/example-app

Build Without paxfile.yml

When the CLI provides the required shape, paxfile.yml is optional:

perl bin/pax build -o ./build/example-app bin/example-app

That keeps one-off builds and self-hosting neutral even inside repositories that ship their own paxfile.yml. Extra roots, assets, and CPAN policy files must be declared explicitly on the CLI in that mode.

Inline entrypoints use the same public surface. -I adds Perl library roots, -M loads/imports modules before execution, and -e supplies the entrypoint code directly:

perl bin/pax build \
  -I lib \
  -MDateTime \
  -e 'print DateTime->now'

pax run accepts the same switches:

perl bin/pax run \
  -I lib \
  -MDateTime \
  -e 'print DateTime->now'

Self Compile

PAX can build itself:

perl bin/pax build -o /tmp/pax bin/pax
/tmp/pax help

That self-built binary can then build another standalone application from its own paxfile.yml. A self-built standalone pax binary can also rebuild from another standalone pax binary input after the original source tree has been removed, because the rebuild path carries an embedded source snapshot for the application units it needs to rebuild.

For plain executable Perl scripts, the standalone build path now keeps source analysis static. pax build does not need to execute a long-running script just to inspect it, and recognized numeric loop subs can be rebound through packaged native artifacts before the script's top-level work starts.

ARCHITECTURE

Entrypoint and Build Configuration

PAX::CLI is a small public facade. It resolves build and run inputs from CLI arguments plus PAX::Paxfile, then delegates to the standalone image builder.

Compilation and Code Units

PAX::CodeUnitCompiler compiles supported Perl source shapes into PCU records. Unsupported or partially supported module shapes use hybrid or fallback payloads so correctness is preserved while reusable compiler support expands.

Dependency Discovery

PAX::StandaloneImage follows entrypoints, library directories, source roots, and cpanfile inputs to collect application modules and dependency payloads. The mechanism is structural and path/module based, not tied to a project name.

Asset Embedding

Individual assets and asset directories are embedded into the executable payload. The generated runtime extracts them into a private runtime directory and exposes that location to the packaged program.

Native and Fallback Dispatch

PAX can package native artifacts for supported hot regions. Runtime dispatch uses native execution when assumptions hold and falls back to bundled Perl payloads when they do not.

Standalone Launcher

The final output is an executable launcher containing package metadata, code units, dependency payloads, optional native artifacts, assets, and runtime helper code.

EXAMPLES

Build from paxfile.yml:

perl bin/pax build

Build a specific entrypoint:

perl bin/pax build -o ./build/example bin/example

Run after building:

perl bin/pax run -- status

Embed application assets:

perl bin/pax build \
  --name webapp \
  --lib lib \
  --source-root lib \
  --asset-dir share \
  --cpanfile cpanfile \
  --runtime-mode bundled_perl \
  --output ./build/webapp \
  bin/webapp

Build PAX itself:

perl bin/pax build -o /tmp/pax bin/pax
/tmp/pax help

Web Applications

PAX supports the single-binary packaging shape for framework applications that combine Perl modules, PSGI or web framework code, templates, CSS, JavaScript, and other static assets.

The validated SOW-03 proof includes a Dancer2 + Plack/Starman + Template Toolkit web application packaged as one executable.

DOCKER DEPLOYMENT MODEL

PAX supports a minimal multi-stage image pattern:

FROM perl:5.42 AS builder
WORKDIR /workspace
COPY . /workspace
RUN cpanm --installdeps .
RUN perl bin/pax build --output /out/app

FROM debian:bookworm-slim
COPY --from=builder /out/app /usr/local/bin/app
CMD ["/usr/local/bin/app"]

The final image contains only the executable. The source tree, assets, cpanfile, and framework installation are builder-stage inputs.

For an external application, the validated packaging pattern is:

  1. build a standalone pax binary

  2. copy that pax binary into the application build stage

  3. compile the application into its own standalone binary

  4. copy only that final binary into the runtime stage

ADAPTIVE COMPILATION RULE

When a module or framework fails under PAX, fixes should improve a reusable compiler, packaging, loader, or runtime path for arbitrary modules of the same class. A project-specific branch is not complete when a neutral generalized implementation is locally actionable.

RELEASE GATES

Release readiness requires:

  • Changes, README.md, cpanfile, dist.ini, and lib/PAX.pm.

  • canonical version synchronization across all PAX modules.

  • POD and README parity for public behavior.

  • make doc-gate, which includes POD-DOC-ALL for the full maintained Perl surface plus changed subroutine comments.

  • make test.

  • make release-gate.

  • a deliberate version-bump step such as make cpan-bump-version VERSION=<next-version> followed by a meaningful top entry in Changes.

  • make cpan-build and make cpan-gate.

  • make git-gate on the committed tree.

  • make push-gate as the final closure gate that pushes the committed HEAD to origin.

cpan-gate verifies that release tarballs exclude temporary probes, generated workspaces, planning artifacts, and other non-release paths. Git cleanliness and forbidden tracked-file checks belong to the separate final git-gate.

make cpan-release follows the DD-style PAUSE flow: run the repo gates, locate the built tarball in the repository root, and upload it with cpan-upload using the local uploader configuration. After a successful upload, the release flow must retag the released commit as RELEASED_TO_PAUSE and push that tag to origin.

The version bump happens before dzil build, for example with make cpan-bump-version VERSION=<next-version> or make cpan-auto-bump. After the bump, the operator must write a meaningful top Changes entry for that version and commit the release-preparation changes. After a successful PAUSE upload, the operator must move RELEASED_TO_PAUSE to the released commit and push the tag to origin. make cpan-dist and make cpan-build then enforce the version gate, the Changes gate, and the documentation gate for README.md, this module POD, and the full maintained Perl surface through POD-DOC-ALL without mutating tracked source files during the packaging step.

TESTING AND COVERAGE

Primary validation from a repository checkout is:

make tdd-gate
make bdd-gate
make atdd-gate
make qa-gate
make test
make release-gate
make cpan-build
make cpan-gate

Completion requires the full chain, not a partial subset. release-gate, cpan-gate, git-gate, or push-gate alone are not sufficient. In project rules, "all gates" means the full closure sequence from TDD through the final push gate. make all-gates is only the convenience target for replaying that final verification set. Treat the change set as complete only when the full gate chain has closed, the committed tree passes git gate, and the committed HEAD has been pushed to origin.

KNOWN LIMITATIONS

  • Dynamic loading and runtime mutation can require fallback paths.

  • Native speed depends on region selection and guard validity.

  • Performance is still shape-driven today. PAX is not hard-coded for one application or module name, but it currently accelerates some Perl code shapes better than others. Tight integer arithmetic loops, repeated numeric leaf routines, and structurally predictable hot paths are strong candidates. Dynamic metaprogramming, runtime-heavy startup, IO-dominated scripts, irregular control flow, and broad general-purpose Perl that never lowers into a native region are weaker current candidates.

  • Practical examples matter more than slogans. The current strong cases are integer sum-loop workloads that PAX can lower into a native region safely. The following five snippets were measured on this repository's 0.030 toolchain on the local Linux/x86_64 build host:

    Invoice rollup:

    use strict;
    use warnings;
    use Time::HiRes qw(time);
    sub sum_to_n {
        my ($n) = @_;
        my $sum = 0;
        for (my $i = 1; $i <= $n; $i++) {
            $sum += $i;
        }
        return $sum;
    }
    sub invoice_rollup {
        my ($lines, $tax_basis) = @_;
        my $subtotal = sum_to_n($lines);
        my $tax = sum_to_n($tax_basis) & 0xFFFF;
        return ($subtotal ^ $tax) & 0x7fffffff;
    }
    my $start = time();
    my $out = 0;
    for my $batch (1..8) {
        $out ^= invoice_rollup(500_000_000, 50_000);
    }
    print "elapsed=", time() - $start, "\n";

    Measured runtime: stock Perl 120.35s, standalone 1.26s, about 95.4x faster.

    Retry budget planning:

    use strict;
    use warnings;
    use Time::HiRes qw(time);
    sub sum_to_n {
        my ($n) = @_;
        my $sum = 0;
        for (my $i = 1; $i <= $n; $i++) {
            $sum += $i;
        }
        return $sum;
    }
    sub retry_budget {
        my ($attempts) = @_;
        my $budget = sum_to_n($attempts);
        return ($budget >> 3) & 0xFFFFFFFF;
    }
    my $start = time();
    my $acc = 0;
    for my $svc (1..8) {
        $acc += retry_budget(500_000_000);
    }
    print "elapsed=", time() - $start, "\n";

    Measured runtime: stock Perl 116.70s, standalone 1.27s, about 91.6x faster.

    Shard weight planning:

    use strict;
    use warnings;
    use Time::HiRes qw(time);
    sub sum_to_n {
        my ($n) = @_;
        my $sum = 0;
        for (my $i = 1; $i <= $n; $i++) {
            $sum += $i;
        }
        return $sum;
    }
    sub shard_weight {
        my ($events) = @_;
        my $w = sum_to_n($events);
        return (($w << 1) ^ ($w >> 5)) & 0x7FFFFFFF;
    }
    my $start = time();
    my @weights;
    for my $shard (1..8) {
        push @weights, shard_weight(500_000_000);
    }
    my $acc = 0;
    $acc ^= $_ for @weights;
    print "elapsed=", time() - $start, "\n";

    Measured runtime: stock Perl 117.49s, standalone 1.19s, about 98.4x faster.

    Backfill window checksum:

    use strict;
    use warnings;
    use Time::HiRes qw(time);
    sub sum_to_n {
        my ($n) = @_;
        my $sum = 0;
        for (my $i = 1; $i <= $n; $i++) {
            $sum += $i;
        }
        return $sum;
    }
    sub window_checksum {
        my ($n) = @_;
        my $v = sum_to_n($n);
        return (($v & 0xFFFF) ^ (($v >> 16) & 0xFFFF));
    }
    my $start = time();
    my $checksum = 0;
    for my $window (1..8) {
        $checksum = (($checksum << 5) ^ window_checksum(500_000_000)) & 0x7FFFFFFF;
    }
    print "elapsed=", time() - $start, "\n";

    Measured runtime: stock Perl 117.31s, standalone 1.23s, about 95.1x faster.

    Cohort retention counting:

    use strict;
    use warnings;
    use Time::HiRes qw(time);
    sub sum_to_n {
        my ($n) = @_;
        my $sum = 0;
        for (my $i = 1; $i <= $n; $i++) {
            $sum += $i;
        }
        return $sum;
    }
    sub retention_counter {
        my ($population) = @_;
        my $total = sum_to_n($population);
        return ($total % 1_000_003);
    }
    my $start = time();
    my $acc = 1;
    for my $cohort (1..8) {
        $acc = ($acc * 33 + retention_counter(500_000_000)) % 1_000_003;
    }
    print "elapsed=", time() - $start, "\n";

    Measured runtime: stock Perl 117.10s, standalone 1.16s, about 100.7x faster.

  • By contrast, a normal CLI command such as dashboard version or dashboard ps1 can still be dominated by framework startup, helper dispatch, subprocess work, or other runtime behavior that is outside the current native region coverage.

  • This means PAX is not yet "compile once and every Perl workload becomes Rust-fast." The current model is broader than one-off app patches, but still selective by supported semantic pattern.

  • Bundled runtime executables are larger than wrappers because they carry runtime payloads needed to run without the source tree.

  • Bundled-perl binaries are validated for builder and runtime environments from the same libc family; arbitrary host-built cross-distro portability is not a release guarantee, so multi-stage Docker deployment should build inside the target container family.

  • Docker validation requires local Docker access.

FAQ

Is PAX tied to one specific project?

No. Example applications are validation corpora. Core compiler and runtime logic are expected to stay neutral and reusable.

Does PAX guarantee Rust-like speed for all Perl code?

No. PAX packages the whole application correctly and accelerates hot paths that it can safely specialize. Dynamic regions continue to use fallback execution.

Is PAX still case by case?

Not by project name or package name. PAX should stay neutral across arbitrary Perl applications. But today it is still case by supported code shape. If PAX recognizes a loop or leaf routine class and can lower it safely, it can do very well. The five examples above are proven cases on this host: invoice rollup, retry budget planning, shard weight planning, backfill window checksum, and cohort retention counting. In those runs stock Perl took about 116s to 120s, while the standalone binaries finished in about 1.16s to 1.27s. If the workload stays in dynamic Perl semantics, it will package correctly but may run close to stock Perl speed.

What should operators report as a performance issue?

Report cases where:

  • a standalone binary is slower than stock Perl in a structurally simple workload

  • a hot loop or numeric kernel does not improve when native lowering was expected

  • an application command regresses badly after packaging

  • performance changes significantly across builder/runtime environments

For a useful report, include the command or script, stock Perl timing, PAX build timing, standalone runtime timing, and whether the workload is CPU-heavy, IO-heavy, startup-heavy, or highly dynamic.

Does pax run require a separate app server?

No. Under SOW-03, pax run builds the standalone executable and then executes that binary directly.

Can PAX package web applications with embedded static assets?

Yes. The validated packaging path includes templates, CSS, JavaScript, and framework code embedded into one standalone executable.

FILES

  • bin/pax - public command entrypoint.

  • lib/PAX/ - compiler, packaging, runtime, and validation modules.

  • paxfile.yml - neutral build manifest.

  • README.md - operator documentation.

  • Changes, cpanfile, dist.ini - release metadata.

LICENSE

Copyright 2026 PAX Contributors.

This distribution is licensed under the Artistic License 2.0.

You may use, modify, and redistribute it under the terms of the Artistic License 2.0. The full license text is available at https://opensource.org/license/artistic-2-0.

SECURITY

Security issues should be reported privately before they are discussed in a public issue tracker.

See the repository SECURITY.md for the reporting address, the backup private advisory route, and the reproduction detail needed for triage.

SEE ALSO

The repository README.md mirrors the public command contract and operator workflow documented here.

The internal documentation rule for DD-style parity is recorded in docs/pax-doc-parity.md.