statements - mvx-lang/mvx GitHub Wiki

Statements

Reference, alphabetically. {...} marks optional parts.

Assignment

var = expr
var<a{,v{,s}}> = expr        dynamic-array replace
ARR(i{,j}) = expr            dimensioned element
var += expr                  compound: also -= *= /= and := (append)

target OP= value is shorthand for target = target OP value; := appends (string concatenation). It works on any assignable target.

BEGIN CASE

BEGIN CASE
CASE cond
   statements
CASE 1                       the catch-all
   statements
END CASE

The first true CASE wins.

CALL

CALL NAME(args...)
CALL @VAR(args...)           indirect: VAR holds the subroutine name

Arguments pass by reference. Resolution happens at call time: the program itself, then cataloged LIB/ libraries, linked packages, and the system account.

FUNCTION / DEFFUN

FUNCTION NAME(args...)       first line of a function program
   ...
   RETURN(value)             return a result (RETURN alone returns "")
DEFFUN NAME(argcount)        declare a function so NAME(...) is a call
X = NAME(a, b)               call it in any expression

A FUNCTION is a program that yields a value through RETURN(expr); it compiles and catalogs like a subroutine and shares the same resolution. A caller DEFFUNs the function (before first use) so NAME(...) reads as a call rather than an array reference, then uses it anywhere an expression is allowed — including recursively. Arguments are passed by value.

COMMON

COMMON {/BLOCK/} item, item(dims), ...

DIM

DIM A(rows{, cols})

ECHO

ECHO ON | ECHO OFF

Terminal echo for INPUT (password entry).

EQUATE

EQUATE name TO expr
EQUATE name LITERALLY expr      (LIT is accepted for LITERALLY)

EQU is accepted as a short spelling of EQUATE. A compile-time constant: every later use of name in expression context expands to expr — so EQUATE AM TO CHAR(254) then X<1> = AM needs no runtime lookup, and a numeric equate stays eligible for specialisation (EQUATE SIZE TO 5 ; DIM V(SIZE)). The right-hand side is any expression and may reference earlier equates. Names must be declared before use and cannot be redefined or assigned to.

END / STOP / RETURN / NULL

END ends the program text. STOP ends the program immediately — even from inside a subroutine. RETURN returns from a GOSUB, or from a subroutine when the GOSUB stack is empty. NULL is an explicit no-op — useful as the body of an otherwise-empty THEN or CASE.

EXECUTE

EXECUTE sentence {CAPTURING var} {RETURNING var} {ON ERROR ... END}

Runs a TCL sentence in this session. CAPTURING collects its output as a dynamic array (line per attribute); RETURNING receives the exit status. A select list formed by the sentence (e.g. an EXECUTEd SELECT) is consumed by this program's next READNEXT. PERFORM is a synonym.

The program runs at a level of its own: its own unnamed COMMON, STATUS() and SENTENCE(), sharing this session's open files, locks, select list and transaction. So it sees what this program has already written — including inside an open transaction — and a file opened here is open there.

STOP comes back; ABORT does not. A STOP, with or without a code, returns here and RETURNING carries the code. An ABORT, or a runtime fault, ends this program too — which is what should happen to work that has failed half way through.

ON ERROR is how a program says otherwise:

EXECUTE "PAYMENTS" ON ERROR
   PRINT "that option gave up"      ;* and this program carries on
END

This is for a shell — a login and menu written in BASIC, replacing TCL. An option that gives up should return the operator to the menu, not log them out. Ordinary code should leave the clause off and let the abort through.

A non-zero STOP is a status, not an error: it does not run the ON ERROR body. Only an ABORT or a fault does.

See Replacing TCL.

FOR / NEXT

FOR I = first TO last {STEP n}
   statements
NEXT I

Inside any FOR or LOOP, CONTINUE skips to the next iteration and EXIT leaves the innermost loop.

FORMLIST

FORMLIST dynarray

Installs the attributes of dynarray as the active select list.

GOTO / GOSUB

GOTO label      GO TO label      GOSUB label ... RETURN
ON expr GOTO  l1, l2, ...        computed jump to the expr-th label
ON expr GOSUB l1, l2, ...        computed GOSUB to the expr-th label

A label is either a number at the start of a line (100 …) or a name followed by a colon (RETRY:) — the same as the modern MV dialects. A named label may stand alone or precede a statement on the same line (DONE: PRINT X), and GOTO/GOSUB/ON accept either form. Label names and variable names are separate, so SAVE: (a label) and SAVE (a variable) can coexist. GOSUBs nest to depth 1024. For ON, expr selects a target 1-based; a value outside 1..n falls through to the next statement.

IF

IF cond THEN statement {ELSE statement}

IF cond THEN
   statements
END ELSE
   statements
END

Single-line and block forms mix freely; ELSE without THEN is allowed.

INPUT

INPUT var

Reads one line from standard input. At end of input the program ends.

LOOP

LOOP
   statements                pre-condition body
WHILE cond DO                or UNTIL cond DO
   statements                post-condition body
REPEAT

The test may sit on the same line as LOOP — LOOP WHILE cond DO … REPEAT (and LOOP UNTIL cond DO …) — the classic Pick/R83 compact form, equivalent to putting WHILE/UNTIL on the next line with no pre-condition body. DO is optional.

LOCATE

LOCATE(item, dynarray{, attr#{, value#}}; setting {; order}) THEN/ELSE

Finds item at the given level. setting receives the position — on ELSE, the insertion point (per order: AL, AR, DL, DR) or count+1.

MAT

MAT A = expr                 fill every element
MAT A = MAT B                copy (dimensions must match)

MATPARSE / MATBUILD

MATPARSE arr FROM strexpr {USING delim}
MATBUILD strvar FROM arr {USING delim}

The in-memory pair to MATREAD/MATWRITE, with no file. MATPARSE splits a string across a DIM'd array — the last element absorbs any overflow, short input leaves trailing elements empty. MATBUILD joins the elements back into a string, dropping trailing empties. The delimiter defaults to @FM; USING gives any string (MATBUILD S FROM P USING ",").

PRINT / CRT

PRINT expr {: expr ...} {,} {:}

: between items concatenates; , advances to the next 18-column zone; a trailing : suppresses the newline. Cursor addressing and colours are just strings — see the terminal chapter.

SEND / MESSAGE

SEND expr TO port {THEN ... {ELSE ...}}
MESSAGE ON | OFF | DEFER

Message another port, and choose what this session does with arriving messages. SEND takes THEN when the message reached somebody, and ELSE when that port is not logged on, when the permit is missing, or when there is no message service running — the program keeps going either way.

TO takes a port number, as D3 spelled it. A user, an account or every port at once goes through MSGSEND (see Intrinsics), which is also where a message carrying a record is built.

Neither word is reserved. MESSAGE is exactly the name a program would give a variable, so each is a statement only in a shape that cannot be anything else:

SEND = "still a variable"
MESSAGE = "so is this"

TRANSACTION

TRANSACTION START  THEN ... {ELSE ...}
TRANSACTION COMMIT THEN ... {ELSE ...}
TRANSACTION ABORT

Bracket several writes so they commit or roll back as one unit. Without a transaction each WRITE is already atomic on its own — with a mapping, the record, its parent columns and its child rows land together or not at all — and this widens that guarantee across statements.

START and COMMIT take THEN/ELSE, as they do on UniData and UniVerse. ABORT takes neither: discarding is best effort by definition, so there is no failure to branch on.

OPEN "ORD" TO F ELSE STOP
TRANSACTION START ELSE PRINT "no transaction" ; STOP
WRITE HEADER ON F, ID
WRITE DETAIL ON F, ID:".D"
TRANSACTION COMMIT ELSE PRINT "commit failed"

The transaction need not be one block. It can be started in one place, written to by subroutines called later, and committed somewhere else again — which is how screen handlers on UniData have always used it:

TRANSACTION START ELSE STOP
GOSUB WRITE.THE.ORDER
GOSUB WRITE.THE.LINES
TRANSACTION COMMIT ELSE PRINT "commit failed"

@TRANSACTION is 0 outside one and non-zero inside, so a subroutine can tell whether its caller already opened one:

IF @TRANSACTION THEN
   GOSUB JUST.WRITE
END ELSE
   TRANSACTION START ELSE STOP
   GOSUB JUST.WRITE
   TRANSACTION COMMIT ELSE PRINT "commit failed"
END

Three rules are worth knowing before you rely on it:

  • One transaction, one connection. The first write inside a transaction binds it to that file's backend and location; a write to a file on a different connection is refused and says so. Two backends committing separately would be atomic in each and in neither overall.
  • A refused write means the transaction cannot commit. Once any write inside one has been refused, COMMIT fails and rolls the rest back, so a program that handles the refused write by logging it cannot leave half its work committed. Use ABORT to give up on purpose.
  • A backend without transactions refuses the write. sqlite, postgres and mysql can hold one; dir, lmdb and mongo cannot, so a write to one of those inside a transaction fails rather than being quietly committed on its own — soft under ON ERROR, fatal without. See Files for which backend an account is using.
  • Ending without committing rolls back. STOP, a STOP with a code, a fault or a kill all discard an open transaction, so the abort path needs no explicit ABORT — which is how the pattern above survives a program that dies half way through a screen.

A second START while one is open is refused rather than nested. UniVerse does allow nesting; MVX does not, because unwinding one level of a nest needs savepoints in the backend, and refusing is better than appearing to nest and then discarding everything on the inner ABORT.

File statements

OPEN {"DICT",} "file" TO fvar THEN/ELSE
CLOSE fvar                            release the file (and its connection)
READ var FROM fvar, id THEN/ELSE
READU var FROM fvar, id THEN/ELSE     read with record lock
WRITE expr ON fvar, id                releases the lock
WRITEU expr ON fvar, id               keeps the lock
READV  var FROM fvar, id, n THEN/ELSE read attribute n of a record
READVU var FROM fvar, id, n THEN/ELSE same, with a record lock
READU  var FROM fvar, id LOCKED ... END THEN/ELSE   (LOCKED: held elsewhere)
WRITE  expr ON fvar, id ON ERROR ... END           (ON ERROR: backend fault)
WRITEV  expr ON fvar, id, n           replace attribute n, keep the rest
WRITEVU expr ON fvar, id, n           same, keeps the lock
MATREAD  arr FROM fvar, id THEN/ELSE  fields (@FM) -> array elements
MATREADU arr FROM fvar, id THEN/ELSE  same, with a record lock
MATWRITE  arr ON fvar, id             elements -> record; releases the lock
MATWRITEU arr ON fvar, id             keeps the lock
DELETE fvar, id
RELEASE {fvar, id}                    bare form releases all
SELECT fvar                           form the active select list
READNEXT var THEN/ELSE                next id from the active list

A select list left unconsumed when a program ends passes to the next command in the TCL session.

CLOSE releases a file. Every file in one database shares one connection, and the connection goes back when the last file on it closes — so a program that walks a list of companies, or an account that is logged away from, does not accumulate them. A file variable stops being one after CLOSE, and using it says so.

Files are closed for you when the session ends, and when a LOGTO leaves the account they belong to. A handle held across a LOGTO — in COMMON /FILES/, say — is no longer open, and reports that rather than reading a handle that has gone. (This is where MVX differs from UniData and UniVerse, whose handles name a file rather than a place in an account's store, and survive. An MVX handle is resolved through the account's bindings, so one that survived would follow you into the new account rather than keep pointing where it was opened.)

A LOGTO is refused while a transaction is open: it belongs to a connection in the account being left, so the program commits it or ABORTs first.

Conditional compilation

A source preprocessor runs before parsing, in the UniVerse/UniData style, so the same source can target several MultiValue systems:

$DEFINE name {value}     define a symbol (optional replacement value)
$UNDEFINE name           remove a definition
$IFDEF name              keep the block if name is defined
$IFNDEF name             keep the block if name is NOT defined
$ELSE                    the other branch
$ENDIF                   end the conditional

MVX is predefined by the mvx-basic compiler; define your own on the command line with -D NAME or -D NAME=value. A $DEFINE with a value also substitutes that value wherever the name appears (outside string literals). Inactive lines are blanked, not removed, so error messages and the source-level debugger still line up with the original file.

$IFDEF MVX
   OPEN "ORDERS" TO F ELSE STOP
$ELSE
   OPEN "", "ORDERS" TO F ELSE STOP     ;* UniVerse two-argument form
$ENDIF

This is the portability seam: commit an account to git on one platform, mvx-git clone it on MVX, and — with the $IFDEFs set correctly — it compiles and runs.

Source inclusion

$INCLUDE item            splice a sibling source file
$INSERT item             synonym for $INCLUDE
$INCLUDE file item       splice file/item (the directory-file layout)

$INCLUDE and $INSERT pull another source file in where the directive stands — the classic way to share EQUATEs, COMMON blocks, and DEFFUN declarations across programs. The target resolves relative to the including file's directory (a .b suffix is tried as a fallback), includes may nest, and $DEFINE/$IFDEF state carries across the boundary. A compiler tracks each spliced line back to its origin, so a compile error in an included file reports that file and line, and the source-level debugger still steps the right code.

⚠️ **GitHub.com Fallback** ⚠️