Perl 5.8.4 for VAX OpenVMS 7.3

Perl 5.8.4 for VAX OpenVMS 7.3

My VAXstations now have Perl: Perl 5.8.4, as a binary kit for OpenVMS VAX 7.3 that needs no compiler on the machine it is installed on. It comes as a BACKUP saveset with a DCL installer and a DCL uninstaller, it passes all but 8 of the 871 files of Perl’s own test suite, and it no longer dies when a number gets too big – which the Perl 5.8.4 release does on a VAX, for something as small as print 1.7e38.

Claude Code assisted, which is to say, did much of the hard work.

Downloads, with SHA-256 sums in SHA256SUMS.TXT:

Perl is under the Artistic License or the GPL, at your choice; the source kit is the corresponding source.

Why 5.8.4

Three reasons, none of them deep:

  • Its readme.vms still describes building on a VAX with DEC C.
  • Craig A. Berry’s VMS build kit for it, perlbuild584.zip from May 2004, is on the DECUS/OpenVMS freeware mirror with the whole release inside, so there was a known source to start from.
  • The ready-made alternatives turned out to be for other machines: the “Perl” on the HP freeware CD is an Alpha kit, and the 5.8.4 binaries on the mirror are for Alpha and Itanium.

I have not tried to build anything newer on the VAX.

The machines

  • A VAXstation 4000/60 (80 MB) and a VAXstation 4000/96 (128 MB), both OpenVMS VAX 7.3 on ZuluSCSI disks, both headless.
  • Simulated MicroVAX 3900s (SIMH) with the same VMS, for anything that might go wrong, and because they are many times faster.
  • Compaq C V6.4-005 and MMS V3.7.

The first build was on the 4000/60. The kits you can download were built on the simulator, then installed and tested on both real machines.

Building and testing, measured

On the 4000/60, the unpatched release:

StepTime
The whole build, all standard extensions and every Encode tableabout 6 h 40 min
Perl’s test suite, 871 test files, 73,592 testsabout 8 h 46 min of test time
Result766 files passed, 96 skipped, 9 failed

Perl needs a page file quota above 100,000 pages to build, according to readme.vms; I built with 131,072.

On the simulator, a pair of clean builds with and without DEBUGGING, timed one after the other on an otherwise idle machine:

DEBUGGINGno DEBUGGING
Configure1 min 10 s1 min 4 s
Build (MMS)38 min 31 s34 min 36 s
Test suite43 min40 min

That pair had only the first fix described below. The two builds that became the kits were made the next day with the whole patch (the DEBUGGING one took 37 min 34 s; the other shared the machine with a test run, so its time means nothing). Their results:

DEBUGGING kitdefault kit
Test suite767 passed, 96 skipped, 8 failedthe same, file for file
PERLSHR.EXE2131 blocks1916 blocks

Simulator times say nothing about a real VAX except the ratio between the two columns.

The eight failures that remain are all explained. One is a DCL command-line length limit in the test for perl -P. One is a test with a two-digit year that now means 2070. The other six are big-number tests (one of them passes all its tests and is counted as failed only because it now prints a warning), and they are the subject of the next section.

The bug hunt: print 1.7e38

A VAX double (D_FLOAT) stops at about 1.7 x 10^38, and there is no infinity. A result that does not fit is a hardware fault. Perl 5.8.4 as released, on the 4000/60:

$ perl -e "print 1.36514538e37, qq(\n)"    ! 1.36514538e+37
$ perl -e "print 1.7e38, qq(\n)"
%SYSTEM-F-FLTOVF_F, arithmetic fault, floating overflow at PC=0016D2D1, PSL=03C00000

  Improperly handled condition, image exit forced.

and then a register dump, and Perl is gone. Not even eval helps: the process image has been killed. 1.7e38 is a perfectly good D_FLOAT number, and readme.vms promises that overflow while converting a string to a number is handled silently. That is how t/op/pack fails: one constant in the test file, 1.36514538e67, kills Perl before the first test.

Where. No debugger was needed. The PC in the crash, a link map of PERLSHR, and a machine-code listing of numeric.c put it on one addd3 instruction in Perl_my_atof2. Perl reads the digits before and after the decimal point separately, scales each by a power of ten, and adds the two. The scaling has a VMS-only guard against overflow. The addition has none. For 1.7e38 the integer part has already been pushed to the largest number by that guard, and adding the fraction to it overflows.

Upstream. I looked at numeric.c in later Perls (5.8.9, 5.10.1, 5.14.4, 5.20.3, 5.28.3 and 5.38.0): the guard is there in all of those, and so is the unguarded addition. There was nothing to backport.

The first fix was small: saturate that addition. The obvious test, “is hi greater than MAX - lo”, turned out to be wrong in a rare case, a rounding tie that hides the overflow. The test that is exact is hi/2 + lo/2 > MAX/2, because halving a binary floating-point number loses nothing.

Then everything else. A literal was only the most embarrassing way to overflow. $x * 10, $x + $x, 2 ** 200, hex of a long string, List::Util::sum: each one killed Perl the same way. The C library, it turned out, does not: pow, exp and strtod quietly return the largest number. So I made Perl’s own arithmetic do what the C library already does: give the largest number, and warn about it under use warnings. On the simulator, with the patched DEBUGGING build:

$ PERL "-w" -e "use B::Deparse; my $x = 9**9**9; my $y = 1.7e38; my $z = $y * 10; print qq(loaded\n)"

Floating point overflow in multiplication (*), result set to 1.70141e+38 at -e line 1.
loaded

(9**9**9 is there for a reason. My first version also warned when the C library’s pow saturated, and Perl’s test suite promptly failed two tests it had been passing: B::Deparse writes 9**9**9 when it means infinity, so every program that loaded it got a warning. The warning now appears only where Perl used to crash.)

A safety net. Perl’s own arithmetic is not the only code that can overflow; any compiled extension can. So there is also a VMS condition handler that turns a floating fault into an ordinary Perl error. This was harder than it sounds. A handler may only unwind to a stack frame that has itself established a handler, so one handler in main crashed with an access violation; the handler had to go into the macro Perl uses everywhere it sets up an eval. On the 4000/60:

$ PERL SAFETYNET.PL 1000
fault inside eval is caught                  ok  (Reserved floating point operand in unpack)
1000 faults in a loop, all caught            ok  (1000 caught)
nested eval: inner catches                   ok
$SIG{__DIE__} is called                      ok  (Reserved floating point operand in unpack)
fault inside a sort block is caught          ok  (Reserved floating point operand in unpack)
fault inside an overloaded + is caught       ok  (Reserved floating point operand in unpack)
ordinary arithmetic afterwards               ok  (5050)
SAFETYNET: 7 ok, 0 not ok
SAFETYNET: now a fault outside eval (the script should die here)
Reserved floating point operand in unpack at safetynet.pl line 15.
%SYSTEM-F-ABORT, abort

And a hang that went away. In the release, a script that sets $SIG{FPE} and then overflows never finishes: VMS restarts the faulting instruction after the handler returns, it faults again, forever. With the safety net the script gets a Perl error in under a second of CPU (on the simulator; the unpatched Perl ran into the 30-second CPU limit I gave it).

Before and after, on the 4000/60. The test in the examples archive has 33 cases, each run as a separate Perl. With the release, 6 ran and 27 killed Perl. With the kit, all 33 do what they should:

case 11: MAX + MAX                                        => MAX  [warning: Floating point overflow in addition (+), result set to MAX]
case 13: MAX/2 * 4                                        => MAX  [warning: Floating point overflow in multiplication (*), result set to MAX]
case 31: XS overflow inside eval (Time::HiRes::alarm)     => caught: Floating point overflow in subroutine entry
case 32: reserved operand inside eval (unpack d)          => caught: Reserved floating point operand in unpack

The whole patch changes eight source files and is conditional on “VMS without IEEE floating point”. It is in the source kit as VAXFP_OVERFLOW.DIFF.

DEBUGGING or not

On VMS, Perl’s Configure builds with DEBUGGING unless told otherwise, so that is what my first kit had. The kit without it is smaller and faster, and gives the same test results, so it is now the default. Five small workloads, CPU seconds, three runs each, on the simulator:

Workloadno DEBUGGINGDEBUGGINGCost
hash14.6315.31+4.7%
sort14.2015.11+6.4%
regex8.549.49+11.0%
string4.444.83+8.8%
numeric8.119.60+18.4%
total49.9254.34+8.8%

I have not run this on a real VAX; I would expect the ratio to hold and the absolute times not to.

Installing

You need OpenVMS VAX 7.3, SYSTEM (or SYSPRV), and about 122,000 free blocks on the system disk. No compiler.

  1. Get one saveset and the two .COM files into one directory on the VAX. Transfer modes matter: the .BCK in binary, the .COM files in ASCII.

  2. A binary transfer loses the saveset’s record attributes. Put them back:

     $ SET FILE/ATTRIBUTE=(RFM:FIX,LRL:32256,MRS:32256,RAT:NONE) -
           PERL584_VAX_20260930.BCK
    

    (The installer refuses the kit and prints this command if you forget.)

  3. As SYSTEM:

     $ @PERL584_INSTALL
    

    It checks privilege, space and the saveset, restores the tree to SYS$COMMON:[SYSLIB.PERL584...], writes a manifest of every file it installed, and runs a smoke test. It took 35 seconds on the simulator, 3 minutes on the 4000/96 and 6 minutes on the 4000/60. It changes nothing else on the system: no startup file, no SYLOGIN, no DCLTABLES, no system logical name.

  4. Each user, at the prompt or in LOGIN.COM:

     $ @SYS$COMMON:[SYSLIB.PERL584]PERL_SETUP.COM
     $ PERL -v
    

@PERL584_UNINSTALL CHECK says what an uninstall would remove, and @PERL584_UNINSTALL does it. It deletes only files the manifest lists and that are still exactly as installed, one explicit DELETE file;version at a time, and keeps and lists anything you changed or added. VMS file versions make that easy to get right. Besides many runs on simulators, I uninstalled it once on the 4000/96: CHECK found all 1487 files unchanged, the uninstall took 4 minutes 13 seconds and removed the whole tree, nothing else on the system changed (I compared directory listings of SYS$COMMON:[SYSLIB], SYS$MANAGER and SYS$STARTUP before and after), and a fresh install afterwards took 3 minutes and passed the same tests as before.

Perl that knows it is on VMS

The kit has the VMS extensions, and they are the fun part. Three small programs from the examples archive, with their output on the 4000/60:

DCL symbols, read and set from Perl (VMS::DCLsym):

DCLSYM: PEX_INPUT (LOCAL table) has 6 numbers; sum 90, max 31, sorted 4,5,9,15,26,31

RMS record files, fixed and variable length (VMS::Stdio::vmsopen):

RECORDS: PEX_FIXED.DAT: 5 records read back, RMS says FIX 32
RECORDS:   [record 1: 1 squared is 1        ]
RECORDS:   [record 2: 2 squared is 4        ]
...
RECORDS: PEX_VAR.DAT: 5 records read back, RMS says VAR 0
RECORDS:   [record 1: 1 squared is 1]

File versions. On VMS glob("*.*;*") returns every version of every file:

my %v;
for my $path (glob("${dir}*.*;*")) {
    my ($name, $ver) = $path =~ /([^\]>]+);(\d+)$/ or next;
    push @{ $v{uc $name} }, [$ver, $path];
}
VERSIONS: A.TXT        3 version(s), highest ;3, 36 bytes; oldest 'first A', newest 'third A'
VERSIONS: B.TXT        1 version(s), highest ;1, 10 bytes; oldest 'only B', newest 'only B'
VERSIONS: unixify gives /SYS$SYSDEVICE/HOME/USER1/LOGIN.COM; vmsify gives it back as SYS$SYSDEVICE:[HOME.USER1]LOGIN.COM

(My first RECORDS.PL wrote its five variable-length records as one, because Perl buffers print. The first test passed anyway: it checked the file’s record format, not its records.)

Known limits

  • Numbers still stop at 1.7e38. An overflow now gives the largest number instead of killing Perl, but the largest number is not the right answer. Six of the eight test files that still fail are big-number tests. The clearest case: Math::BigRat’s bsqrt of 1/3 turns a value of about 40 digits into a native number on the way, which now saturates instead of crashing, and the result is a wrong fraction. Keep big values as objects or strings.
  • $SIG{FPE} is never called. The fault becomes a Perl error first; use eval.
  • Time::HiRes: a sub-second alarm does not interrupt select().
  • %ENV: assigning to it creates a process logical name that is still there after Perl exits.
  • localtime uses the C library’s time zone rules, which in VMS 7.3 are the US rules from before 2007, so it is an hour off for a few weeks each spring and autumn.
  • No G_FLOAT kit. readme.vms says Perl can be built with /FLOAT=G_FLOAT, which reaches about 9 x 10^307. I have not built it. The patch was written so that it should be right for G_FLOAT too: it never mentions a D_FLOAT constant.
  • Core Perl only: no CPAN modules, no threads.
  • The kit installs to SYS$COMMON:[SYSLIB.PERL584] and nowhere else.

Rebuilding

The source kit’s README.TXT has the steps: unzip the release, copy the eight patched files over it (they are there in full, in case your VAX has no patch), and submit one batch job that runs Configure and MMS. Perl’s source tree is deeper than the eight directory levels ODS-2 allows in one file specification, so everything goes through concealed logical names. That also caught me once at install time: the prefix Configure wrote, SYS$COMMON:[SYSLIB.PERL584.], is a concealed root inside a concealed root, and RMS will not have it. One line of the generated PERL_SETUP.COM is changed for that, and that diff is in the source kit too.

How this was done

As with Emacs: Claude sessions with written briefs, on two VAXstations and a simulator, with the rule that nothing counts until a test fails without the fix and passes with it, and the transcripts are kept. Every number above comes from one of those transcripts. Every kit install was tried on a simulator first.

Credits: Larry Wall and the Perl 5 Porters for Perl; Craig A. Berry and the other maintainers of the VMS port, whose readme.vms and build kit made this possible twenty-two years later; and Claude Code (Anthropic’s Claude models) for much of the building, debugging, testing and writing.