Architecture and components
VX/TDMS is a compiler and a linked runtime library, not a long-running daemon. There is
no server process to administer: the "runtime" is libvtdms_forms, linked
directly into the migrated application, and it reads compiled form/request/library/record
definitions from disk the moment a program opens a request library. The table maps each
OpenVMS TDMS component to its VX/TDMS counterpart.
| OpenVMS TDMS component | VX/TDMS counterpart | Role | Status |
|---|
| FDU (Form Definition Utility) | tdmsc compile (form path) | Parses FDU LIST FORM listings into form p-code: geometry, fields, attributes, validators, access order. | live |
| RDU (Request Definition Utility) | tdmsc compile (request path) + C++ front end | Parses .rdf request definitions into request p-code. | live |
| CDD / DDL record definitions | tdmsc compile (record path) | Parses CDD DDL into record p-code with exact byte layout, OCCURS/ARRAY, and variants. | live |
| LDF / RLB request library | tdmsc compile (library path) | Parses .ldf library definitions into library p-code; models request-lookup membership. | live |
TSS$ form manager (linked runtime) | libvtdms_forms | C++ runtime exporting the TSS$ call surface; loads p-code, binds records, renders forms, executes requests. | live |
| FDU interactive form editor | Browser-based form editor | Edits VX/TDMS form p-code (form / layout / assign / order phases). | partial: p-code editor, not a terminal emulator |
| DECforms conversion (VSI OpenVMS) | IFDL translation path | Translates TDMS assets to VSI DECforms IFDL; rewrites TSS$ calls to FORMS$. | partial |
| VX/DECFORMS target (Linux) | VX/DECFORMS compiler, extended | Lowers TDMS request syntax directly into VX/DECFORMS. | v1 |
No server to administer. Unlike ACMS-style multi-process runtimes,
VX/TDMS has no daemon and no IPC substrate to configure. The compiled p-code is read
directly by the runtime library at TSS$OPEN_RLB time.
Call lifecycle / data flow
The compile-time flow runs offline, once per estate:
Source inputsFDU form listingLIST FORM input in .lis format
RDU requestRequest definition input in .rdf format
LDF libraryLibrary definition input in .ldf format
CDD DDLRecord definition input in .ddl format 01 – ConvergeCompile the estatecompile or compile-all selects the parser for each source type.
02Write JSON p-codeProduce the form, request, library, and record p-code formats.
03Validate semanticsvalidate-semantics checks mappings against the compiled form and record definitions.
04 – Optional outputGenerate bindingsCreate language bindings when the migrated application needs them.
The run-time flow happens per interactive session, inside the migrated program:
01Open channelThe program calls TSS$OPEN and receives a channel.
02Open request libraryTSS$OPEN_RLB(lib-file) returns a library ID while the runtime loads request, form, and record p-code.
03 – TSS$REQUEST(ch, lib, name, %REF rec1, rec2)03ADecode recordsRead the caller's record buffers by reference.
03BApply OUTPUT mappingsMove record values into the form fields.
03CRender formPaint the form through the ANSI terminal path.
03DCollect INPUTAccept the interactive field values.
03EValidateRun the form and request validation rules.
03FWrite recordsReturn values through the same record buffers.
03GRun request actionsExecute actions such as RESET, REVERSE, and RING BELL.
04Close request libraryTSS$CLOSE_RLB releases the loaded library.
05 – DoneClose channelTSS$CLOSE(ch, clear) ends the interactive session.
The record buffers passed to TSS$REQUEST are opaque to the caller and shared
with the runtime by reference; the runtime reads and writes fields at the byte offsets
recorded in that request's record p-code. This is the crux of source-level compatibility:
the program's RECORD variable is the same bytes the runtime edits.
Source languages
VX/TDMS compiles four related definition file types. Only the RDF request language is a
full imperative form language; the others are structured metadata.
| File type | OpenVMS origin | Contents |
|---|
*_form.lis | FDU LIST FORM output | Form name, formwide attributes, screen width, help form, field access order, per-field name / picture / length / default / help / attributes / validators / video / scrolling / indexing. |
*_req.rdf | RDU request source | Form binding, record binding, mappings, control fields, program keys, video actions, waits. |
*_lib.ldf | LDF library source | REQUEST IS / REQUESTS ARE, FILE IS, membership. |
*.ddl | CDD/DDL record source | DEFINE RECORD, STRUCTURE, nested structures, OCCURS/ARRAY, variants, field types. |
The RDF request language
A request binds one or more program records to a form and moves data between them. The
example below is a representative request showing the constructs VX/TDMS handles end to
end: record binding with an alias, a program key, top-level and nested
CONTROL FIELD cases, subscripted array mappings, and %LINE loop
variables.
FORM IS ACCOUNT_INQUIRY_FORM;
RECORDS ARE ACCOUNT_REC WITH NAME ACCTREC,
CDD$TOP.BANKING.DATA.SYST_STNDFORM_REC WITH NAME STNDFORM;
DESCRIPTION
/* Displays outstanding statements for an account. Control field is FLAG. */;
RESET FIELD %ALL;
USE FORM ACCOUNT_INQUIRY_FORM;
PROGRAM KEY IS GOLD "E"
NO CHECK;
RETURN "ABORT " TO STNDFORM.RECORD_LOCK;
END PROGRAM KEY;
OUTPUT
ACCTREC.ACCOUNT_ID TO ACCOUNT_ID,
ACCTREC.STATEMENT_NUMBER[1 TO 30] TO STATEMENT_NUMBER[1 TO 30],
ACCTREC.SELECT_STATEMENT[1 TO 30] TO SELECT_STATEMENT[1 TO 30];
CONTROL FIELD IS FLAG
'START ':
OUTPUT STNDFORM.PGM_NAME TO PGM_NAME,
"" TO STATEMENT_DATE[1 TO 30];
INPUT ACCOUNT_ID TO ACCTREC.ACCOUNT_ID;
'ACCT ':
REVERSE FIELD ACCOUNT_ID;
INPUT ACCOUNT_ID TO ACCTREC.ACCOUNT_ID;
'DISPLAY':
CONTROL FIELD IS DATE_FLAG[%LINE = 1 TO 30]
"X":
OUTPUT ACCTREC.STATEMENT_DATE[%LINE] TO STATEMENT_DATE[%LINE];
INPUT SELECT_STATEMENT[%LINE] TO ACCTREC.SELECT_STATEMENT[%LINE];
END CONTROL FIELD;
END CONTROL FIELD;
END DEFINITION;
An empty string literal ("") as an OUTPUT source clears the
receiving field. A CONTROL FIELD nests arbitrarily and its cases are matched
against the named field's value. %LINE and %ENTRY are loop
variables over subscript ranges, and field references may be dotted CDD record paths.
RDF instruction set
| Instruction | Meaning |
|---|
FORM IS <name> / USE FORM / DISPLAY FORM | Bind / select / paint a form (name may be a dotted CDD path). |
RECORD IS / RECORDS ARE … WITH NAME | Bind one or more program records, with an optional alias. |
DESCRIPTION /* … */ | Comment block. |
RESET FIELD %ALL | <list> | Clear all or listed fields. |
PROGRAM KEY IS GOLD "k" [NO CHECK] … END PROGRAM KEY | GOLD-prefixed key handler. |
OUTPUT <src> TO <formfield>, … | Move record or literal value to the form. |
INPUT <formfield> TO <recfield>, … | Move form value to the record. |
RETURN <lit> TO <field> | Assign a literal (also stands alone in control-field arms and program keys). |
CONTROL FIELD IS f[subscript] … END CONTROL FIELD | Case dispatch on a field value; nests arbitrarily. |
[NO] REVERSE / BLINK / BOLD / UNDERLINE FIELD <list> | Video attribute on listed fields. |
RING BELL | Sound the terminal bell. |
SIGNAL OPERATOR / SIGNAL MODE IS REVERSE | Operator signal / signal mode. |
WAIT / NO WAIT | Block / don't block for input. |
CHECK / NO CHECK | Enable / skip field validation. |
CLEAR SCREEN | Clear the screen. |
END DEFINITION | Request terminator. |
Subscripts are multi-dimensional with loop variables: [1 TO 30],
[%LINE], [%LINE = 1 TO 100, %ENTRY = 1 TO 25].
Parenthesised multi-receiver mappings. Some DEC documentation mentions a
parenthesis form for multiple mapping receivers, e.g. (field1, field2) ….
VX/TDMS's compiler accepts the parenthesis-free list form throughout, which is the
canonical RDU form. We have not yet seen a production request that requires the
parenthesised variant.
The TSS$ runtime API
VX/TDMS ships a C++ runtime (libvtdms_forms) that exports the OpenVMS
TSS$ call surface so recompiled application code links unchanged. Each
routine is exported under three names: the VMS dollar-sign name (via an assembler alias,
so existing object references resolve), a portable TSS_ C alias, and a C++
class behind both.
VMS ABI
Return convention: the function result is a VMS condition value returned by
value: odd means success, even means failure. Callers test ODD(...).
Canonical signatures:
ret = TSS$OPEN (channel.wlu.r [, device.rt.dx])
ret = TSS$CLOSE (channel.rlu.r [, clear-screen.rlu.r])
ret = TSS$OPEN_RLB (rlb-file-spec.rt.dx, library-id.wlu.r)
ret = TSS$CLOSE_RLB (library-id.rlu.r)
ret = TSS$REQUEST (channel.rlu.r, library-id.rlu.r, request-name.rt.dx
[, record1.mz.r [, ... recordN.mz.r]])
ret = TSS$READ_MSG_LINE (channel.rlu.r, response-text.wt.dx
[, message-prompt.rt.dx] [, response-length.wwu.r])
ret = TSS$WRITE_MSG_LINE(channel.rlu.r, message-text.rt.dx)
ret = TSS$SIGNAL ; signal last condition, no args
ret = TSS$COPY_SCREEN (channel.rlu.r [, file-spec.rt.dx] [, append-flag.rlu.r])
ret = TSS$DECL_AFK (channel.rlu.r, key-id.rlu.r, key-efn.rlu.r,
key-astadr.szem.r [, key-astprm.rlu.v])Routine table
| Routine | Purpose | Status |
|---|
TSS$OPEN | Open a terminal channel (writes channel out). | live |
TSS$CLOSE | Close channel, optional clear-screen. | live |
TSS$OPEN_RLB | Open a request library by file spec (writes library-id out). | live |
TSS$CLOSE_RLB | Close a request library. | live |
TSS$REQUEST | Execute a named request against zero or more record buffers (variadic, by reference). | live |
TSS$SIGNAL | Signal the last condition value. | live |
TSS$READ_MSG_LINE | Read a response from the message line. | live |
TSS$WRITE_MSG_LINE | Write text to the message line. | live |
TSS$CANCEL | Cancel outstanding I/O on a channel. | live |
TSS$COPY_SCREEN | Copy screen to a hardcopy file (honours the TSS$HARDCOPY logical). | live |
TSS$STRACE_ON / TSS$STRACE_OFF | Screen-trace to a portable trace file. | live |
TSS$DECL_AFK / TSS$UNDECL_AFK | Declare / remove an async function-key handler. | live |
TSS$OPEN_A / TSS$REQUEST_A / TSS$CLOSE_A | Async variants. | partial: complete synchronously, status delivered through the RSB pointer |
TSS$HARDCOPY is honoured as the default TSS$COPY_SCREEN destination
when set as a logical name / environment variable. TSS_TRIGGER_AFK (and
interactive AFK:<key-id> input) fires a declared async function-key
callback with the channel, key-id, and the caller's AST parameter.
Request-execution semantics
TSS$REQUEST does the real work. On every call it:
- Loads the request / form / record p-code for the named request.
- Decodes raw record buffers at compiled offsets:
TEXT, signed/unsigned
integer, DATE, F_FLOATING, G_FLOATING,
UNSIGNED QUADWORD, and BIT all decode to display text and
encode back to native storage bytes on input. - Applies RDF
OUTPUT mappings: record/literal to form, including
control-field cases and indexed repeated fields. - Renders the form: live terminal output uses ANSI cursor addressing and SGR video
attributes; copy-screen output stays plain text for deterministic hardcopy comparison.
- Applies RDF
INPUT mappings, validates the value (required, uppercase,
numeric, choice, range, size, check digit, length), and writes it back into the raw
application record buffer. - Runs request actions: field reset, video attributes,
RING BELL,
SIGNAL OPERATOR, WAIT, NO WAIT.
Real call path (VAX Pascal)
A migrated program links this unchanged against libvtdms_forms; note the
ODD() status test and %REF (by-reference) record passing:
{ open channel + request library }
IF NOT ODD (TSS$OPEN(TDMS_CHANNEL)) THEN TDMS_STATUS := TSS$SIGNAL;
IF NOT ODD (TSS$OPEN_RLB('BANKING_RLB:ACCT_INQUIRY_LIB',
TDMS_LIB_ID)) THEN TDMS_STATUS := TSS$SIGNAL;
{ run a request against two program records, passed by reference }
IF NOT ODD (TSS$REQUEST(TDMS_CHANNEL,
TDMS_LIB_ID,
'ACCOUNT_INQUIRY_REQ',
(STNDFORM), { %REF record 1 }
(ACCTREC))) { %REF record 2 }
THEN TDMS_STATUS := TSS$SIGNAL;
{ … application reads/writes fields of ACCTREC, which the runtime just filled … }
{ tear down }
IF NOT ODD (TSS$CLOSE(TDMS_CHANNEL, CLEAR_SCREEN)) THEN TDMS_STATUS := TSS$SIGNAL;
IF NOT ODD (TSS$CLOSE_RLB(TDMS_LIB_ID)) THEN TDMS_STATUS := TSS$SIGNAL;
The equivalent portable C entry point is declared as
extern "C" int TSS_REQUEST(int channel, int library_id, const char* request_name, ...);,
returning the condition value by value.
Data model: JSON p-code
The compiler emits versioned JSON p-code, one file per source artifact, in four formats.
Every file carries format, version, source,
diagnostics, and summary keys; readers ignore unknown future keys
and error on missing required keys. This is deliberate: reviewable p-code is what makes a
migration regression-testable against the original OpenVMS behaviour, instead of trusting
an opaque binary translation.
| Format | From | Key contents |
|---|
VXTDMS-FORM-PCODE | .lis | meta (form name, line range, screen width), access_order, fields, ops. |
VXTDMS-REQUEST-PCODE | .rdf | name, form_name, records (in TSS argument order), actions, mappings, program_keys. |
VXTDMS-LIBRARY-PCODE | .ldf | name, file, requests. |
VXTDMS-RECORD-PCODE | .ddl | name, root, byte_size, fields (flattened with byte layout). |
A form field entry carries name, source line, row/col, length, display
length, datatype, picture type, attributes, occurs count, index direction, scroll flag, and
validators. An op entry is clear, draw_text, or
field. A request mapping entry carries direction
(OUTPUT/INPUT/RETURN), source, target, source line,
and the active CONTROL FIELD conditions. A record field entry
carries name, full nested CDD path, datatype, element size, byte offset, byte size, and
dimensions (from OCCURS/ARRAY) with per-element offsets.
Integrity properties enforced by the compiler
- Per-element offsets, not contiguous-array assumptions, so nested
STRUCTURE OCCURS and CDD variant overlays are represented faithfully. Fields
from different variant branches may legally share the same offset. - Fixed record byte size is compiled and used to size the raw buffer
the runtime decodes and writes.
- Semantic validation resolves every mapping operand against compiled
form fields and record paths, accepting dotted paths and unique nested-suffix references,
and reports form/record reference errors, missing fields, static subrange violations,
occurrence-count mismatches, and type-category mismatches, each with source path, RDF
line, and severity.
Not a black box. Every construct the compiler cannot represent is counted
and named on stderr with filename, line, and construct text, never silently dropped.
Duplicate form names are uniquified with a warning, not silently.
Build and compile pipeline
The compiler driver is tdmsc. The C++ runtime side builds with
CMake ≥ 3.16 and C++17.
Pipeline steps
| Step | Command | Input | Output |
|---|
| Compile one artifact | tdmsc compile <file> [-o out.json] | One .lis / .rdf / .ldf / .ddl (type by extension) | One *.tdms.json |
| Compile a whole estate | tdmsc compile-all <root> -o <dir> [--validate-semantics] | Tree of forms / requests / libraries / records | Directory of p-code + counters |
| Validate semantics | tdmsc validate-semantics <pcode-dir> | Compiled p-code directory | Mapping diagnostics + counters |
| Render p-code | tdmsc render <pcode> [--values v.json] [--html f.html] | Form p-code | Text or HTML screen |
| Render a request | tdmsc render-request --form F --request R [--records recs.json] | Form + request p-code + record data | Rendered screen after OUTPUT mappings |
| Validate values | tdmsc validate <pcode> <values.json> | Form p-code + field values | Validation messages |
| Generate bindings | tdmsc gen-bindings <record-pcode> -o <dir> [--language c|pascal|all] | Record p-code | .h / .inc byte-layout bindings |
Worked example
# 1: compile the whole estate
tdmsc compile-all /opt/tdms/banking/cdd -o /tmp/tdms_pcode
# → compiled=500 fields=8740 ops=22087 warnings=3 unsupported=0 errors=0
# 2: validate every mapping against compiled forms + records
tdmsc validate-semantics /tmp/tdms_pcode
# → semantic_requests=109 semantic_mappings=5030 semantic_warnings=2 semantic_errors=0
# 3: generate C + Pascal byte-layout bindings for the record p-code
tdmsc gen-bindings /tmp/tdms_pcode -o /tmp/tdms_bindings --language all
On a representative production-scale estate this breaks down as 105 forms, 109 requests, 64
libraries, and 222 records: 500 files, 8,740 fields, 22,087 rendering ops, and 5,030 request
mappings, all validated with zero semantic errors.
Skip/fail discipline. The compiler is not silent best-effort. Every run
prints warnings=, unsupported=, and errors=
counters; an unrepresentable construct is named on stderr with filename, line, and
construct text. Exit code is non-zero on errors.
CLI reference
| Subcommand | Args / flags | Notes |
|---|
compile | input, -o/--output | Type chosen by extension; exit 1 on errors. |
compile-tree | root, -o (required), --pattern | Forms only. |
compile-all | root, -o (required), --validate-semantics | Forms + RDF + LDF + DDL; uniquifies duplicate output names with a warning. |
render | pcode, --values, --html, --no-fields, --no-defaults | |
render-request | --form (required), --request (required), --records | |
validate | pcode, values | Exit 1 if any messages. |
validate-semantics | pcode_dir | Exit 1 on semantic errors. |
gen-bindings | record_pcode, -o (required), --language {c,pascal,all} | |
Incremental rebuilds
compile recompiles a single artifact to a single p-code file. There is no
daemon to reload. The runtime reads p-code fresh at TSS$OPEN_RLB time, so a
recompiled p-code file takes effect on the next run. The browser form editor writes a
timestamped backup beside the file before replacing it.
Modernization paths and divergences
VX/TDMS supports three target runtimes off the same compiled inventory:
form listings, requests, libraries, and record metadata. Path 1 keeps the application on
VX/TDMS's own TSS$ runtime on Linux; Paths 2 and 3 convert the application to a
DECforms-family target.
| Item | Path 1: VX/TDMS runtime | Path 2: TDMS → VSI DECforms | Path 3: TDMS → VX/DECFORMS (Linux) |
|---|
| Compile forms/requests/libraries/records to reviewable p-code | live | live (source for the translator) | live |
Run original TSS$ calls unchanged | live | n/a: calls rewritten to FORMS$ | n/a: calls rewritten to FORMS$ |
| Semantic validation of all mappings | live | live | live |
| Translate to DECforms IFDL | out of scope | partial | via VX/DECFORMS TDMS extensions |
TSS$ → FORMS$ program conversion | out of scope | partial | partial |
| TDMS request syntax lowered directly by the forms compiler | n/a | out of scope: VSI DECforms is native | v1: design target 99.99% automated |
Async TSS$*_A fidelity | partial: synchronous completion via RSB | reported | reported |
| Interactive FDU terminal-keypad editing | out of scope: p-code editor only | out of scope | out of scope |
What DEC's own converter drops, and what VX/TDMS preserves
Path 2 targets the native DECforms product and inherits DEC's own Converter limitations,
documented in the DECforms TDMS Conversion Guide. That guide states plainly that
the Converter converts forms and requests but does not convert the
program, and, even for forms and requests, does not convert eight specific things. VX/TDMS
was built to preserve every one of them, because it compiles from source rather than from a
built .RLB file.
| DEC Converter will not convert | Why DEC drops it | VX/TDMS status |
|---|
| CHECK DIGIT field validator | DECforms has no CHECK DIGIT validator. | Preserved: VX/TDMS compiles and executes the validator. |
| Default key definitions | DECforms binds built-in functions to keys; only ERROR/GOLD/HARDCOPY are emulated. | Modelled: PROGRAM KEY IS GOLD "k" … END PROGRAM KEY is represented natively. |
Dependent ranges + nested CONTROL FIELD IS | DECforms has no looping mechanism and no case structure, so you redesign by hand. | Preserved: the biggest single differentiator. Nested control fields and %LINE/%ENTRY dependent ranges parse and validate cleanly. |
DESCRIPTION instruction | DEC reads the built .RLB, which does not retain comment text. | Available: VX/TDMS compiles from RDF source, so the text is present. |
LIGHT LIST instruction | Not every terminal has the referenced LEDs. | Path-3 mapping to an operator message is a planned equivalent. |
| Operator input order (ORDER phase) | The Converter doesn't maintain the field order you specify. | Preserved: VX/TDMS records access_order in every form p-code. |
| Non-default scale factors | The Converter converts only the default scale factor from the picture. | Preserved: VX/TDMS emits the compiled scale_factor into form p-code fields. |
Values in RING BELL (up to 255) | DECforms SIGNAL takes no count. | Not yet exercised against a request that specifies a count. |
DEC's own prerequisites for Path 2. The Converter only handles forms and
requests built with TDMS ≥ 1.7. It also does not support CDD/Plus record variants or all
CDD/Plus data types. DEC requires removing variants and changing unsupported field types
by hand before conversion. VX/TDMS represents variant branches as overlay offsets and
decodes the full datatype set directly, so this prerequisite does not apply on the VX
paths.
Path 3: the compatibility superset
Rather than force every TDMS concept into standard DECforms, VX/DECFORMS is extended to
understand TDMS request syntax and runtime behaviour directly, and the same migration
rewrites TSS$ calls to FORMS$. Because the compiler lowers TDMS
syntax natively, most of the limitations above simply don't arise on this path: nested
CONTROL FIELD IS, dependent ranges, program keys, explicit and subrange
mappings, video attributes, and operator signals are all recognized constructs. Where a
behaviour still needs project-specific policy, the compiler emits a compatibility
diagnostic rather than silently changing meaning.
Scope honesty. Path 3's DECforms-lowering and the TSS$ →
FORMS$ rewrite are under active development today. The claim above is
grounded in VX/TDMS's verified p-code coverage of these constructs, not yet in a measured
end-to-end Path 3 conversion of a full production estate.
Deliberate divergences (Path 1 runtime behaviour)
| Divergence | Reason |
|---|
TSS$OPEN_A / TSS$REQUEST_A / TSS$CLOSE_A complete synchronously, writing status through the RSB pointer | Linux has no VMS AST/$WAITFR kernel primitive; the caller's $WAITFR/RSB sequence still observes completion correctly. |
Live rendering uses ANSI/SGR; TSS$COPY_SCREEN output is plain text | Deterministic hardcopy and byte-stable regression comparison against reference captures. |
TSS$STRACE_ON/OFF writes a portable trace file | No VMS screen-trace facility exists on Linux; the trace is logged messages, screens, and copies. |
| The form editor edits VX/TDMS JSON p-code, not byte-for-byte CDD/FDU storage | It is a p-code editor, not an FDU terminal emulator; full FDU keypad equivalence is future work. |
Validation-failure behaviour, message-line geometry, and .RLB/.FRM binary layouts are VX-defined in places | The most detailed DEC Request/Programming/Forms/Reference manuals are unrecoverable; where public documentation is silent, VX/TDMS defines and documents its own behaviour. |
Operator and runtime management
There is no server to administer. Runtime behaviour is controlled by logical names (honoured
as environment variables) and a handful of files.
| Control | Effect |
|---|
TSS$HARDCOPY / TSS_HARDCOPY | Default destination file for TSS$COPY_SCREEN when no file argument is given. |
VXTDMS_PCODE_DIR | Directory the runtime loads compiled p-code from. |
VXTDMS_INPUT_<FIELD> | Supplies an INPUT field value in non-interactive runs (otherwise the current form value; interactive runs prompt on stdin). |
AFK:<key-id> (interactive input) / TSS_TRIGGER_AFK | Fires a declared async function-key callback. |
Screen-trace file (TSS$STRACE_ON/OFF) | Portable log of rendered request screens, messages, and copy-screen operations. |