replacing tcl - mvx-lang/mvx GitHub Wiki
Most MV sites do not put operators at a command prompt. They put them in a login program and a menu — CueBic, SB+, or something the site wrote — and TCL is either hidden or reached only by people who are trusted with it. Sites that want a command language of their own write one.
MVX is built to be removed from that picture. mvx is a shell like any other
program: a prompt, a builtin table, and dispatch. Everything underneath it —
the session, the files, the locks, the transactions, the ability to run a
program — belongs to the runtime, and your program can have it instead.
This page is how.
A cataloged main program is an executable that owns a session. So the whole of
"replacing TCL" is: run your program instead of mvx.
* MENU -- catalog this and run it instead of mvx
COMMON /FILES/ F.ORD, F.CUST
OPEN "ORD" TO F.ORD ELSE PRINT "cannot open ORD" ; STOP
OPEN "CUST" TO F.CUST ELSE PRINT "cannot open CUST" ; STOP
LOOP
PRINT
PRINT " 1) Orders 2) Customers Q) Log off"
PRINT " choice: ":
INPUT CH
UNTIL OCONV(CH, "MCU") = "Q" DO
BEGIN CASE
CASE CH = 1 ; EXECUTE "ORDER.ENTRY"
CASE CH = 2 ; EXECUTE "CUSTOMER.ENQUIRY"
CASE 1 ; PRINT " no such option"
END CASE
REPEATmvx -a /srv/acct -c 'CATALOG BP MENU' # once
cd /srv/acct && MVXACCOUNT=. ./CATALOG/MENUSet that as the operator's login command and they never see a prompt.
A verb reached by EXECUTE runs in this process, at a level on your
session. It gets its own unnamed COMMON, its own STATUS and its own
SENTENCE(), and shares everything the session owns. Three consequences,
and they are the reason this is worth doing:
Files opened once stay open. The menu above opens ORD and CUST at
login and puts them in named COMMON. Every option that declares the same
block gets the handles — no OPEN anywhere else, and no reopening per screen.
* ORDER.ENTRY -- no OPEN in it at all
COMMON /FILES/ F.ORD, F.CUST
READ ORDER FROM F.ORD, ID THEN ... A transaction can span screens. Start one in a pre-save, let the operator work, commit it at the next screen. The transaction belongs to the session, not to whichever program happens to be running.
TRANSACTION START ELSE PRINT "cannot start" ; STOP
EXECUTE "TAKE.PAYMENT" ;* writes inside the transaction
EXECUTE "POST.LEDGER" ;* and so does this
TRANSACTION COMMIT ELSE PRINT "commit failed"A program reached by EXECUTE reports @TRANSACTION as 1 and sees what the
caller has already written. This is not what other MV systems do — on
jBASE and UniData an EXECUTE'd program sits outside the caller's transaction
and reads a stale view, with nothing reported. MVX diverges deliberately; see
DECISIONS.md.
A select list simply persists. EXECUTE a SELECT, then READNEXT in
the caller — no file is handed sideways between processes, because there is
only one process.
EXECUTE "SELECT ORD"
LOOP
READNEXT ID ELSE EXIT
...
REPEAT| the prompt | libedit history, the command stack (.L, .R, .X), .C macros |
| crash isolation | a verb you run in-process can take the session down — there is no forked child between you and it |
The first is usually the point.
An ABORT, or a runtime fault, takes the calling program with it. That is
UniData's behaviour and MVX matches it: a program that EXECUTEs one that
aborts does not resume either. It is right for ordinary code — work that has
failed half way through should not carry on as though it had not.
A shell is the exception, and says so:
EXECUTE "PAYMENTS" ON ERROR
PRINT "that option gave up" ;* back in the menu loop
ENDWithout the clause the abort passes through, exactly as before. With it, the menu survives the option.
On UniData there is no equivalent: the abort is caught by TCL, so a site that removes TCL loses the only thing that was catching one. This is one of the few places MVX adds rather than matches, and the reason is that MVX means removing TCL to be a supported thing to do.
A non-zero STOP is a status and not an error — RETURNING carries it, and
the ON ERROR body does not run.
For a host that is not BASIC — an application server, a terminal concentrator, something embedding MVX — the surface is five calls.
#include "mvx_runtime.h"
#include <dlfcn.h>
mvx_ctx *ctx = mvx_ctx_create(); /* the session */
for (;;) {
char *line = your_menu_reads_a_choice();
char verb[128], path[2048];
/* your own idea of what an option means, or VOC's: */
if (mvx_voc_lookup(ctx, verb, path, sizeof path) != 1) continue;
/* the library suffix first, then the plain path */
char lp[4200];
snprintf(lp, sizeof lp, "%s%s", path, LIB_SUFFIX); /* .so / .dylib */
void *h = dlopen(lp, RTLD_NOW | RTLD_LOCAL);
if (!h) h = dlopen(path, RTLD_NOW | RTLD_LOCAL);
mvx_program_fn fn = (mvx_program_fn)dlsym(h, "mvx_main");
int aborted = 0;
int64_t rc = mvx_level_run_at_prompt(ctx, fn, line, &aborted);
if (aborted) show("that option gave up"); /* you are still here */
}
mvx_ctx_destroy(ctx); /* log off */mvx_level_run_at_prompt is what makes you the abort boundary: it runs a
program at a level and stops an abort there instead of passing it on. That is
the only difference between it and mvx_level_run, which is what EXECUTE
uses.
mvx_voc_lookup is optional. It gives you VOC resolution — the account, then
linked packages, then the system account — but nothing stops a menu from
mapping option 3 to a path of its own.
Loading a program: try the platform's library suffix first, then the plain
path. On Linux CATALOG/<name> is a small loader and CATALOG/<name>.so is
the program; on macOS the one file is both. See Packages.
A program takes its account from the working directory, so a login does:
cd /srv/acct
MVXACCOUNT=. exec ./CATALOG/MENUmvx -a <dir> does the same thing before showing its prompt.
Entering an account runs its VOC LOGIN, if it has one — a V record naming
a program, the same shape as any verb, so CATALOG BP LOGIN is all it takes.
It runs on mvx -a, and on every LOGTO, which is where it earns its keep: a
menu that moves to SALES gets SALES's setup without knowing what that is.
It is the account's own — never inherited from a linked package or the
system account, because a LOGIN that came from behind would run in every
account on the machine with nothing naming where it came from.
It runs on entering an account, not per program: a cataloged program run straight from Unix pays nothing, so the login still is your program when you start one that way.
A LOGIN that gives up is reported and does not take the caller with it — by
the time it runs, the session is already in the new account, so killing the
menu that moved there would be the worst of both. And a LOGIN that itself
does a LOGTO does not start another one.
A menu that picks a company or a division needs to move, and LOGTO() is how:
IF LOGTO(DIVISION) THEN
GOSUB OPEN.THE.FILES
END ELSE
PRINT "cannot log to ":DIVISION:", status ":STATUS()
ENDIt returns 1 when the session moved and 0 when it did not — STATUS() is 1
for an account that cannot be entered and 2 for a transaction still open,
which is the one refusal a shell should expect to handle rather than report.
The program keeps running either way.
Everything the old account held is gone: its files are closed, its locks
dropped, its connections given back, and the VOC, package and LIB/ chains
forgotten. A handle kept in COMMON from before the move says it is stale
rather than reading somewhere the operator no longer is — so re-opening is
part of the move, and belongs in the GOSUB above.
A transaction is not settled for you. Commit it or ABORT it first; moving
account with one open would commit into somewhere the program no longer is.
EXECUTE "LOGTO <dir>" does the same thing, because that is the spelling
UniData and UniVerse use and ported code will already have it.
-
TCL and Verbs — what
mvxdoes, if you want to reproduce part of it -
Statements —
EXECUTE,TRANSACTION,COMMON - Packages — how a cataloged program is published