Vim 9.2 for VAX OpenVMS 7.3

Vim 9.2 for VAX OpenVMS 7.3

My VAXstations now have Vim 9.2 (patch level 491, February 2026 source): the character-cell build for OpenVMS VAX, installed for all users with a DCL installer and a DCL uninstaller. It also turns syntax highlighting on without crashing, which took a bug hunt.

The first kit (October 14) installed the prebuilt vim.exe that Zoltan Arpadffy’s Polarhome service builds, exactly as supplied. That program had a defect: with syntax highlighting or file-type detection on, it ended with “Caught deadly signal SEGV” on .TXT files, on an empty buffer and in :help. The new kit, kit C, has a VIM.EXE I compiled from the matching VMS source with a six-line fix in Vim’s C code. The cause turned out to be neither the compiler nor the VAX. It was a C union read through a different member than the one written, which every other platform happens to tolerate. The story is below. A second small fix came along the way: Vim on VMS now reads a .VIMRC made with EDIT or CREATE properly, instead of as one line.

The current kit, kit E2 (October 27), is the same VIM.EXE with a system-wide vimrc in one of two modes, chosen at install time: COLOUR, syntax colouring on for every user (my 4000/90 and 4000/96 run that), or PLAIN, colour off unless you ask for it and no standard plugins except matchparen, for the slow machines (my 25 MHz 4000 VLC, where a start with colour took 36 seconds).

Claude Code did much of the hard work.

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

The bug hunt

(M) Everything in this section was measured, on a simulated MicroVAX 3900 (SIMH, OpenVMS VAX 7.3, the same Compaq C 6.4) unless it says Linux.

Narrowing it down. The crash came when Vim’s filetype detection fell back to its Vim9 script autoload/dist/script.vim. I cut that script into pieces:

  • Compiling all of it (:defcompile) worked. Running it crashed, so the fault was in execution, not in the compiler of Vim9 script.

  • Its big function, DetectFromText, is one long if/elseif chain. With all 46 branches it ran. With the final else block added, it crashed.

  • That else block, split into one construct per test, gave the answer. Every function with a :while loop crashed, and nothing else did, down to

    def F()
      var n = 5
      while n < 3
        n += 1
      endwhile
    enddef
    

Vim told me why. :disassemble F printed 4 WHILE $1 -> 655360. The loop ends at instruction 10, and 655,360 is 10 × 65,536. Another function said -> 720896 = 11 × 65,536 where 11 was right. The jump target was stored 16 bits too high. When the loop condition was false, Vim jumped to instruction 655,360 of a 13-instruction function.

The cause. Vim compiles each instruction into a C struct whose arguments sit in a union. A plain jump keeps its target in jump_T, { enum; int }. A :while keeps it in whileloop_T, { short; int }. At :endwhile the compiler fills in the targets of all the pending jumps, and it writes them through jump_T, the :while instruction included. Execution then reads it back through whileloop_T.

With normal alignment both ints sit at offset 4, so this works on every other machine. DEC C on VAX does not pad structures by default. I measured it with a ten-line C program:

Compaq C qualifierssizeof(whileloop_T)offset of while_endoffset of jump_wherewhile_end after writing 11 as jump_where
default624720896
/MEMBER_ALIGNMENT84411

So it was not the compiler and not the VAX. It was a C portability bug that only a compiler that packs structures could expose. My own rebuild from the unmodified source crashed in exactly the same way, so a rebuild alone could not have fixed it.

The fix is to write the member the instruction actually uses:

	isn = ((isn_T *)instr->ga_data) + cur->el_end_label;
	if (isn->isn_type == ISN_WHILE)
	    isn->isn_arg.whileloop.while_end = jump_where;
	else
	    isn->isn_arg.jump.jump_where = jump_where;

Building the whole of Vim with /MEMBER_ALIGNMENT would also have hidden it. But that changes the layout of every structure, including the ones that go to the VAX C library and system services, and it would fix nothing for anyone else.

Are there more like it? (M, Linux) I built the same Vim source on Linux with Vim’s own types packed to one byte (#pragma pack(1), the VAX layout) and without :terminal and channels, as on the VAX. The packed build crashed on the same test. With the fix, all 14 of Vim’s own test_vim9_* suites gave exactly the same results as a normal build. Without it, three suites failed, the ones with :while loops. Every other place that patches an earlier instruction writes the right member.

Installing

You need OpenVMS VAX 7.3, SYSTEM (or SYSPRV), about 180,000 free blocks while installing (the 94,563-block saveset and the 80,000 blocks of the installed tree), and patience. The restore of about 4,770 files takes 10 to 12 minutes on a VAXstation 4000/90 or 4000/96, and 34 minutes on a 25 MHz 4000 VLC.

  1. Put the saveset and the .COM files into one directory. .BCK in binary, .COM in ASCII.
  2. $ @VIM_INSTALL restores the tree into SYS$COMMON:[SYSLIB.VIM], verifies it, writes a manifest and smoke-tests it with a non-interactive edit. @VIM_INSTALL "" REPLACE replaces an installed copy (kit B or C, for example; 15 to 19 minutes on a 4000/90 or 96). Nothing is changed outside that directory.
  3. Each user: @SYS$COMMON:[SYSLIB.VIM]VIM_SETUP. It defines VIM, VIMRUNTIME and the commands VI, VIM, VIEW, XXD and EXCTAGS.
  4. $ @VIM_UNINSTALL removes exactly what the manifest recorded, and keeps any file that was changed.

Options are quoted, because the VMS C run-time makes unquoted command lines lower case: -R is written -/r (that is how Vim’s own os_vms.txt spells it), and -u NONE must be written -u "NONE". Unquoted, -u none names a file called none: Vim says E282 and loads all the plugins anyway. My first kit’s README made exactly that mistake in its “reproducers”.

The second fix: .VIMRC files made with EDIT

(M) The first kit C test run turned up an older defect. A file Vim sources (your .VIMRC, :source) was read line by line only if it was Stream_LF. A file made by CREATE, EDIT or FTP in ASCII mode has variable-length records, and Vim read all of it as one line, with errors like E121: Undefined variable: echomsg.

The cause is one word. On VMS, Vim opens scripts in binary mode ("rb"), and in binary mode the VAX C library returns a variable-length file’s records with nothing between them. Opening scripts in text mode ("r") on VMS only fixes it. Variable-length, VFC and a 5,000-character line all now read correctly, Stream_LF is unchanged, and startup time is the same within the noise. Your .VIMRC can now be written with any editor.

One DCL trap remains: COPY VIM:VIMRC_EXAMPLE.VIM SYS$LOGIN:.VIMRC makes VIMRC_EXAMPLE.VIMRC, because DCL fills the missing file name in from the input. To start from the example, let Vim write it:

$ VIM -es -u "NONE" -i "NONE" -c "write sys$login:.vimrc" -c "qa!" VIM:VIMRC_EXAMPLE.VIM

Syntax colouring for everyone (kit D2)

(M) Would colour even work? The VAXen are used over Telnet, and VMS decides the terminal type at login (SET TERMINAL/INQUIRE): from my Linux terminal it says VT200_Series, No Ansi_Color. That looked bad. But VMS Vim takes its terminal name from the C library (vt200-80), and it matches every vt1..vt5 name to its built-in vt320 description, which has 8 colours and ANSI colour codes. VT100, VT400 and even UNKNOWN (which falls back to vt320) all came out as t_Co=8, and the session logs are full of ESC[34m comments and ESC[33m keywords. A real monochrome DEC terminal should simply ignore those codes; I have not tried one yet.

So the kit now installs a system vimrc, VIM:VIMRC. (what :version calls $VIM/vimrc):

filetype plugin indent on
if &t_Co > 2
  syntax on
endif

It is read before your own SYS$LOGIN:.VIMRC, so syntax off there still wins. One subtlety: Vim loads defaults.vim, which does the same two things, only when you have no vimrc of your own. The first version (kit D) ran them anyway, so for users without a .VIMRC syntax was loaded twice. Kit D2 first checks whether Vim will find a vimrc, exrc, VIMINIT or EXINIT of yours (the same list, in the same order, as Vim’s main.c) and does nothing if not. One thing the test turned up: on VMS, Vim’s “second user vimrc”, sys$login:vimfiles/vimrc, does not name SYS$LOGIN:[.VIMFILES]VIMRC.; Vim never reads it.

What colour costs (VAXstation 4000/96, starting Vim on a two-line C file, 3 runs each; the “now” column is measured, the “before” column partly derived):

WhoBefore (kit C)Now (kit D2, M)
You have no .VIMRC (defaults.vim already turned syntax on)the same as now (derived: the system vimrc does nothing for you)7.5 s
You have a .VIMRC without syntax linesmore than 2.6 s (measured with -u, which leaves 'compatible' on and skips most plugins)7.4 s
vim -u "NONE" (nothing at all)1.1 s (M)1.1 s

By difference (derived, not profiled: this build has no --startuptime), filetype detection plus C syntax is roughly 4 s of that and the standard plugins roughly 2 s. On the VAXstation 4000 VLC (25 MHz) everything takes about 4.7 times as long: 35.8 s with no .VIMRC, 35.5 s with one (more than 9.3 s for the same user without colour, measured the same flawed way), 3.3 s for vim -u "NONE". A user with no .VIMRC waits that long with any kit, because it is Vim’s own defaults.vim that turns syntax on, which is why kit E (below) switches it off there.

I got the baselines wrong twice along the way, both times by trusting a command-line switch to mean what I wanted. vim -u "DEFAULTS" also skips the plugins, so I first concluded that the double syntax on cost 2.5 s; it cost about 0.45 s (the README in kit D2 still repeats the 2.5 s; the next kit fixes the text). And vim -u myfile leaves 'compatible' on, which makes most plugins quit at once, so my “without colour” figures for users with a .VIMRC are too low.

Colour off on the slow VAXen (kit E)

Thirty-six seconds to open a file is too long, so the slow machines now get colour off by default, and anyone who wants it puts it in their own .VIMRC. Rather than two kits, kit E carries both system vimrc files and the installer chooses:

$ @VIM_INSTALL VIM_VAX_92_491E2.BCK REPLACE PLAIN    ! or COLOUR (kit E2, below, installs the same way)

Left out, the choice is made from F$GETSYI("HW_NAME"): PLAIN on a “VAXstation 4000-VLC” or a 3100, COLOUR anywhere else. The manifest records it, so the uninstaller and the next person know.

The PLAIN vimrc has one job for a user with no vimrc of their own: stop defaults.vim (let g:skip_defaults_vim = 1) and do everything it does except filetype detection and syntax: nocompatible (without defaults.vim nothing else turns Vi mode off), scrolloff=5, the jump-back-to-last-position autocommand, and so on. For a user who has a vimrc it does nothing, so one line in your .VIMRC gets the colour back:

source $VIMRUNTIME/defaults.vim

What it saves (M, VAXstation 4000 VLC, seconds per start on a two-line C file, 3 runs each, the real start-up path every time):

Before (kit D2, COLOUR)Now (kit E, PLAIN)
No .VIMRC36.413.0
Own .VIMRC without syntax lines35.512.4
Own .VIMRC with syntax on and filetype plugin indent on37.639.6
No .VIMRC, --noplugin27.14.3
vim -u "NONE"3.33.3

So a start without colour now takes about 13 seconds instead of 36, and colour costs about 27 seconds on this machine, whichever kit is installed (the third row). The standard plugins (matchparen, gzip, zip, tar and the rest) still cost about 9 seconds of the 13, which is the next section.

No plugins on the slow VAXen (kit E2)

This build has no --startuptime, so I timed each plugin by switching it off (its g:loaded_... flag set on the command line, 3 starts each, on the VLC). No single plugin was the problem. With every flag set, 10.2 of the 13 seconds remained: Vim still searches the runtime path and opens all 14 plugin files, only to have each one stop at its first if. What does help is the option that skips the search altogether, 'noloadplugins' (4.6 s), and then loading the one plugin worth having by name (about 4.9 s):

set noloadplugins
runtime plugin/matchparen.vim

So kit E2’s PLAIN vimrc does that, for every user, and one line in your .VIMRC gets all the plugins back (Vim loads them after every vimrc has been read):

set loadplugins

Two things I nearly got wrong. matchparen does nothing while 'compatible' is set, and the system vimrc runs before Vim has found your .VIMRC and switched 'compatible' off. So, as first written, the runtime line would have loaded nothing for exactly the users with a .VIMRC. The vimrc now switches it off for them first, as Vim is about to anyway. And netrw (:Explore, editing a directory) never loaded on VMS at all, in any kit. Its plugin is only packadd netrw, and :packadd looks for plugin/**/*.vim; the VMS port cannot expand ** (Vim’s start-up code even says so and avoids it there). So :packadd finds the package, adds it to the runtime path, and runs nothing. By hand it works:

set runtimepath+=$VIMRUNTIME/pack/dist/opt/netrw
runtime! plugin/netrwPlugin.vim

That one goes upstream with the other findings.

What it saves (M, VAXstation 4000 VLC, seconds per start, 3 runs each):

Kit E (PLAIN)Kit E2 (PLAIN)
No .VIMRC13.25.0
Own .VIMRC without syntax lines12.54.4
Own .VIMRC with syntax on and filetype plugin indent on39.531.3
No .VIMRC, --noplugin4.54.9
vim -u "NONE"3.33.3

From 36 seconds with colour on October 26 to 5 seconds on October 27, on a 25 MHz machine. Even colour, switched on in your own .VIMRC, is 8 seconds quicker, because the plugins are gone from that start too. The --noplugin row went up a little: the system vimrc loads matchparen even then.

The interesting parts

  • ODS-2. Names are at most 39 characters. Eight of the 4,766 files in the runtime archive have longer names (Vim’s own syntax-test inputs and expected screens); UNZIP refuses them, and they are not in the kit, which lists them in README.TXT with sizes and hashes. 39 more, in the source archive’s test directory, could not be unpacked for the build either; nothing in the build needs them.
  • Eight directory levels. Seven runtime files lie deeper than that below SYS$COMMON:[SYSLIB], and RMS refuses such names. They can only be reached through a rooted logical name (DEFINE/TRANSLATION=CONCEALED), so the installer restores through one. Restoring a saveset through a rooted name must say ROOT:[*...]. ROOT:[000000...] makes a real subdirectory called 000000; I did that once in this round.
  • Vim cannot use a rooted name as VIMRUNTIME. What works is a rooted VIMROOT with ordinary names pointing into it: VIM = VIMROOT:[000000], VIMRUNTIME = VIMROOT:[VIM92].
  • E138 on first use. Vim 9.2 on VMS cannot create the viminfo file, so VIM_SETUP creates an empty .VIMINFO once.
  • The maintainers’ own “modified by” setting does not build on VAX. Setting MODIFIED_BY in Make_vms.mms makes the makefile append a #define to config.h with DCL OPEN/APPEND. DEC C then cannot read the file (%RMS-W-RTB, 76 byte record too large). I put the #define in os_vms_conf.h instead.
  • The test. VIM_TEST.COM makes 19 checks as an unprivileged user: banner, a non-interactive edit that keeps the record format and makes a new version, syntax highlighting, :help files, the optional packages, XXD, CTAGS and the viminfo write. Kit C adds the four cases that used to crash, the regression test and two checks of variable-length scripts; kit D2 adds eight checks of the system vimrc (no vimrc, your own .VIMRC, syntax off winning, .EXRC, VIMINIT, _VIMRC.): 27 in all. Kit E adds syntax on in your own .VIMRC winning, and kit E2 checks which plugins each kind of user gets, set loadplugins bringing them back and NoMatchParen switching matchparen off: 30 in all.

Results (M = measured)

  • (M, simulator) Building my VIM.EXE from the source archive: 24 minutes 52 seconds (unpack 3 min 45 s, compile and link 21 min 5 s), on a SIMH MicroVAX 3900 sharing a busy Linux host (not a VAXstation; real-VAX times to follow).
  • (M, simulator) Before the fix, 8 of the 64 narrowing tests crashed: every one that contains a :while loop. With the fix all 64 run, and the regression test WHILE_TEST.VIM passes.
  • (M, simulator) VIM_TEST as a fresh, unprivileged USER1: 19 of 19 for kit C, in 18 seconds; 27 of 27 for kit D2, in 22 seconds.
  • (M, simulator) The lifecycle test (install, refuse a second install, CHECK, a changed file and a user file survive the uninstall, uninstall, second uninstall says “not installed”, reinstall, REPLACE): 21 of 21 for kit C, in 35 and 47 minutes on two runs (the host was busy), and 23 of 23 for kit D2 (two more checks for the system vimrc), in 30 minutes; each restore of the 4,772 or 4,773 files took 7 to 16 minutes. (An earlier draft said 22 of 22 for kit C: I had counted the summary line.)
  • (M, Linux) Vim’s own Vim9 test suites with the VAX structure layout: the same results as a normal build, with the fix.
  • (M, VAXstation 4000/90) Installing kit C over kit B with REPLACE: 18 minutes 28 seconds (removing kit B 6 min 33 s, restoring 4,772 files 11 min 34 s). VIM_TEST as an unprivileged user: 19 of 19. Interactively, a .VIMRC typed with CREATE, a .TXT file with syntax on, and :help all work – the program the first kit shipped crashed on the last two.
  • (M, VAXstation 4000/96 and 4000/90, October 26) Kit D2 over the installed kit with REPLACE: 15 min 14 s and 18 min 34 s; VIM_TEST 27 of 27 on both; colour checked on the screen.
  • (M, VAXstation 4000 VLC) A fresh install of kit D2: 33 min 58 s (the saveset went over by RCP through an SSH tunnel in 2 min 25 s); VIM_TEST 27 of 27; colour checked. Then kit E in PLAIN mode over it: 58 min 12 s (removing D2 23 min, restoring 34 min); VIM_TEST 28 of 28; the manifest records VIMRC_MODE PLAIN.
  • (M, simulator) Kit E: lifecycle 31 of 31; VIM_TEST 28 of 28 in PLAIN mode and again in COLOUR mode after a REPLACE.
  • (M, simulator) Kit E2: lifecycle 32 of 32; VIM_TEST 30 of 30 in PLAIN and in COLOUR mode; kit E2 installed with REPLACE over kit E, as on the real machine, keeps nothing of kit E and passes again.
  • (M, VAXstation 4000 VLC, October 27) Kit E2 in PLAIN mode over kit E with REPLACE: 58 min 16 s (removing kit E 23 min, restoring 34 min; the saveset went over by RCP in 2 min 25 s); VIM_TEST 30 of 30; a start with no .VIMRC 5.0 s (it was 13.2 s).
  • The 4000/60 and the 3100/M38 are switched off until November; they get kit E2 then (the 3100 in PLAIN mode), and the 4000/90 and 96 move from D2 to E2 in COLOUR mode.

Things this does not do

  • :help needs a window. Run from a batch job (no terminal), Vim sees a screen of 2 lines by 12 columns and says E36: Not enough room. Interactively it works.
  • :packadd runs no plugin file on VMS (the ** pattern above), so netrw and matchit do not load unless you load them by hand.
  • No GUI (the VAXen are headless), no Python/Perl/Lua, no clipboard.
  • The fixes have not gone upstream yet.

Licence

Vim is distributed under the Vim licence, which is charityware (:help uganda). Unmodified parts may be passed on with the licence text. The runtime files, XXD.EXE and CTAGS.EXE in the kit are unmodified. My VIM.EXE is a modified Vim, which the licence also allows on conditions:

  • the licence text goes with it, unmodified;
  • the changes go with every copy (the three diffs above, which are also in the saveset);
  • there is a contact for the modified version (me);
  • :version and the intro screen say it was modified.

Mine say Modified by Douglas P. Fields, Jr. <symbolics@lisp.engineer>. My changes are under the Vim licence. The installer, uninstaller, setup procedure and tests are mine.

What the package is

Polarhome’s vim-92-runtime.zip (the runtime files) and vim-92-exe-vax-term.zip (from which the kit keeps XXD.EXE and CTAGS.EXE), and my VIM.EXE. I built it from Polarhome’s vim-92-src-vms.zip (vim92-491) with Compaq C V6.4-005 on OpenVMS VAX V7.3 and MMS, with the same compiler options as theirs (/optimize /prefix=all, MODEL = HUGE, terminal only). It is a terminal-only build (+syntax, +acl, +vim9script; no GUI, no Python, no job or channel). The archives and their hashes are listed in SOURCES.TXT.