-
Notifications
You must be signed in to change notification settings - Fork 0
Porting BASIC between MV systems
One rule underpins everything below:
Compile and run on every target before trusting a change. Each platform accepts things the others refuse, so a single-platform green is not evidence.
This page records differences that have actually cost time, with the evidence for each. It covers MVX, Rocket UniData 8.3, Rocket UniVerse 14.2 and jBASE 6.2.
They are not equally dangerous, and the ordering matters when you are deciding how much to test:
- Refuses to compile. Cheap: you find out immediately.
- Compiles, then misbehaves. Expensive, but a test will catch it.
- Compiles everywhere and differs silently. Worst, because nothing fails anywhere — you get a wrong answer, or a protocol that stalls with no error.
Most of this page is class 2 and 3.
Verified on jBASE 6.2.1.1:
SUBROUTINE TOUT(OUT)
OUT = 1
RETURN
"TOUT", line 1 (offset 13) near RESERVED WORD "OUT":
The same source with the parameter renamed GOUT compiles clean. OUT is
fine on MVX, UniData and UniVerse, so this only appears when jBASE first
compiles the code — by which time the name may be spread over dozens of files.
mv_git carried OUT in 22 BP/ items and had to rename all of them.
If you are writing shared code, do not name anything OUT.
LN is the natural-log function, so LN = DCOUNT(X, @AM) is a syntax error —
and the compiler reports the NEXT line as a second error too, so one bad name
reads like two faults:
"MVPKGOS", line 383 (offset 2) near RESERVED WORD "LN":
"MVPKGOS", line 391 (offset 2) near RESERVED WORD "NEXT":
2 errors were found
OUT is not special. Verified on 6.2.1.1, every one of these fails as a
variable or parameter name:
| name | seen in |
|---|---|
OUT |
22 files in mv_git |
SUB |
SUBROUTINE X(SUB, …) |
SENTENCE |
SUBROUTINE X(SPEC, SENTENCE, SKIP) |
STATUS |
STATUS = 6 |
KEY |
KEY = FIELD(S, " ", 3) — GIT.CONFIG, GITCONFIG, GIT.ATTR
|
DIR |
GIT.CLONE line 46, GITCLONE line 2 |
COUNT |
GITLOG line 2 — note this is also a function on every MV system |
DATA |
GIT.OSFILE line 32 |
LN |
MVPKG.INSTALL, MVPKG.REMOVE — jBASE's natural-log function |
NEG |
MVPKG.INSTALL — jBASE's negate function |
The message names the word, which makes these cheap once you are compiling on jBASE at all:
"GIT.CMD.ADD", line 20 (offset 12) near RESERVED WORD "SUB":
SUBROUTINE GIT.CMD.ADD(SUB, DESC, HANDLER)
^
syntax error
The cost is in when you find out. All six compile fine on MVX, UniData and UniVerse, so they accumulate freely until jBASE first sees the code — in mv_git they had reached 16 programs, and because the catalog step discarded its output the failures were invisible: the suite reported a clean run while a third of the verb's subroutines were simply absent from the library.
If you are writing BASIC that may reach jBASE, treat this as a naming rule rather than a debugging exercise.
LOCATE ... SETTING makes SETTING a keyword, so a GOSUB label named for what
the routine does is a compile error on UniData while compiling clean on MVX:
main program: syntax error at or before
<line 23> GOSUB SETTING
------------------^
Misuse of reserved word 'SETTING'
Expecting: number,variable
main program: syntax error at or before
<line 61> SETTING:
------^
Misuse of reserved word 'SETTING'
Both the GOSUB and the label itself are reported, so — as with jBASE's LN —
one bad name reads as two faults. Worth noting because the usual advice is to
watch variable names: a label is just as exposed, and label names tend to be
plain English verbs (SETTING, STATUS, DATA) which is exactly the vocabulary
these dialects keep for themselves.
Found in MVPKG.CONFIG (mv_package#30), which had compiled on MVX first.
$IFDEF asks the preprocessor about a symbol, and PLATFORM.H is where the
build declares them. A source that guards a branch with $IFDEF ENGINE but
never says $INCLUDE BP.INC PLATFORM.H does not fail to compile — it quietly
compiles the $ELSE arm. Everywhere.
This is the most expensive bug shape in this document, because the compiler
that is easiest to develop against can hide it. mvx-basic predefines MVX
and ENGINE, so on MVX the guarded branch is taken regardless of what the
source says. On UniData and UniVerse $ELSE happens to be the correct answer,
so nothing looks wrong there either.
In mv_git, fifteen handlers were in this state. It surfaced only when jBASE
became the first platform where ENGINE arrives only through PLATFORM.H —
and there all fifteen took a dispatch path with no arms for them, so most of the
verb answered unknown git-object operation.
Two rules:
- Any source that uses
$IFDEFmust include the header that declares the symbols. No exceptions — a guard reading an undeclared symbol is not a guard. -
Test it structurally, because at runtime it is invisible on the platforms
where
$ELSEis right anyway:
for f in BP/*; do
grep -q '$IFDEF' "$f" || continue
grep -q 'INCLUDE BP.INC PLATFORM.H' "$f" || echo "unguarded: $f"
doneCompiler-builtin defines are a convenience that becomes a trap the moment a second implementation exists. Prefer the header on every platform, including the one that does not need it.
The three-argument CONVERT function is not the same function everywhere:
| platform | signature |
|---|---|
| MVX, UniData, UniVerse | CONVERT(from, to, expr) |
| jBASE | CONVERT(expr, from, to) |
Both compile on both. Neither warns. The wrong order simply returns something plausible, which is the worst possible failure for a data-transformation call.
Measured on 6.2.1.1 — converting attribute marks to newlines:
S = "aa" : CHAR(254) : "bb"
X = CONVERT(CHAR(254), CHAR(10), S) ;* jBASE: returns CHAR(254). One byte.
Y = CONVERT(S, CHAR(254), CHAR(10)) ;* jBASE: "aa" LF "bb". Correct here.In mv_git this converted LF to SRC inside the single character AM and
handed that character back, so a whole attribute record staged as two bytes
— an attribute mark and a newline. Nothing was ever declared, every later read
fell back to live values, and twelve assertions failed in ways that all pointed
at the wrong place.
Guard it, do not guess:
$IFDEF JBASE
OUT = CONVERT(SRC, AM, LF)
$ELSE
OUT = CONVERT(AM, LF, SRC)
$ENDIFThe statement form is not the escape. CONVERT chars TO chars IN var works
on jBASE and on U2, but MVX rejects it — expected '=' in assignment. So
there is no single spelling of CONVERT, function or statement, that compiles
and behaves on all four.
Often you do not want CONVERT at all. The commonest use is "clean up a
shell capture", and FIELD says that portably in one spelling:
* the FIRST LINE of a capture, with any mark or CR ahead of it dropped
LINE1 = FIELD(FIELD(FIELD(X, @AM, 1), CHAR(10), 1), CHAR(13), 1)Verified identical on MVX, UniData 8.3, UniVerse 14.2 and jBASE 6.2.1.1.
It recurred. This page already carried the warning above when mv_package#54
was filed, and mvpkg still had the U2 order in six places — because the
symptom never looked like CONVERT. There it turned MVPKGOS "PLATFORM" into
udt:<LF>:<LF>:le, the newlines went into a registry query string, the HTTP
call failed, and every install reported "not found in registry". A documented
trap is not a fixed one: when you meet this, grep the whole tree for CONVERT(
rather than fixing the site in front of you.
There is no shell-escape verb on jBASE — and none is needed, which is easy to
get backwards. EXECUTE runs the command itself:
EXECUTE "echo hello" CAPTURING C ;* C<1> = "hello"
EXECUTE "CREATE-ACCOUNT /tmp/x" CAPTURING C ;* really creates the accountWhat does not work is the wrapper other platforms use. On UniData and UniVerse
you reach the OS through SH -c '...'; on jBASE SH is not a command, so that
form fails with SH: No such file or directory — and so do !cmd, SHELL cmd
and DOS cmd. It is easy to read those four failures as "no shell access" and
conclude the platform cannot do it at all. Drop the wrapper instead.
Two consequences worth planning for:
-
No shell means no shell syntax. No
2>&1, no pipes, no globbing, no quoting rules.CAPTURINGtakes stdout only, so anything the command reports on stderr goes to the terminal rather than into your variable. -
A spawned MV binary makes its own session. If it calls the session factory
— as any tool built on the jBASE API does — an install whose licence counts
sessions pays for two while it runs. The same is true of shelling out to
udt-gitoruv-giton the other platforms. Fine for a one-off like clone; wrong for anything in a loop.
It takes stdout alone, and there is no status to inspect afterwards. The
workaround every other platform uses is unavailable too: jBASE's argument
handling means /bin/sh -c "cmd 2>&1" arrives as a filename for sh to run.
So a command that failed and explained itself on stderr reaches BASIC as an
empty variable — indistinguishable from a command that succeeded quietly. If
what you are running can fail in a way you must react to, run it from C
(popen with 2>&1) and hand BASIC both the text and the status.
Both take the process down with exit 201 and no message at all — the CRT
before the CALL prints, the one after does not:
-
-std=c11is strict ISO C.strdup,popen,pcloseand the<sys/wait.h>status macros are POSIX, so they are not declared, and an undeclared function is assumed to returnint— which truncates a 64-bit pointer.free()on it kills the process.#define _POSIX_C_SOURCE 200809Lbefore the includes. -
Return values come back as the DEFC's result, not through an extra
VAR. Storing an integer into aVARthe caller passed fails the same silent way.
The compiler warns about the first (implicit declaration of function) if you
are reading build output — which is a good reason not to send it to /dev/null.
KEYIN() reads a pipe perfectly well when it owns one, so a full-screen program
can be driven headlessly. It cannot be driven through jsh: jsh reads its
own input line-buffered, so piping a sentence followed by keystrokes leaves
KEYIN() with Error getting input from STDIN , errno = 0 and jsh then runs
the keystrokes as its next command. Invoke the cataloged program directly and
give it stdin.
jbase_env.sh puts jBASE's bin ahead of everything, and it ships its own
sort. Any shell script that sources the environment and then pipes through
sort gets jBASE's, which answers:
No file name could be found for your query
It is the only coreutil shadowed on 6.2.1.1 (grep, uniq, head, tail,
wc, find, sed and awk all still resolve to /usr/bin), which is what
makes it easy to miss — everything else in the pipeline behaves. The failure
looks like a quoting or working-directory problem rather than the wrong binary,
and it produced several confidently wrong diagnoses here before it was spotted.
Use /usr/bin/sort in any script that sources jbase_env.sh.
GETENV("HOME") compiles on jBASE and then fails at RUN time with
Error in Conversion Code "HOME"
Unknown conversion code.
which reads as a bad OCONV mask rather than a missing function -- the argument
you passed is quoted back at you as though it were a conversion code. It is
already on the not-everywhere list below; the reason it earns a section is that
the error points at the wrong thing, so it survives a casual read of the output.
Found by running mvpkg on jBASE 6.2.1.1: 21 env reads across 9 programs, and only 3 of them were in the OS seam.
OPEN "VOC" fails on every jBASE account -- there is no such file. The account
dictionary is MD (240 records in a freshly created account).
Do NOT guard each open. The name is one fact, and a codebase that opens the
master dictionary in fourteen places would carry fourteen copies of it. Declare
it once in the generated PLATFORM.H:
EQUATE MVMASTER TO "MD" <- jBASE
EQUATE MVMASTER TO "VOC" <- MVX, UniData, UniVerse
and write OPEN MVMASTER TO F. Two details matter:
-
EQUATE, not a valued$DEFINE. UniData has no value-substitution form of$DEFINE, so$DEFINE MVMASTER "VOC"will not substitute there.EQUATEto a string literal is ordinary MV BASIC and compiles on all four. -
Bake the value in; do not
$IFDEFinside PLATFORM.H. A nested guard is precisely the construct that compiles on UniData and fails on UniVerse, and a generated per-platform file has no reason to carry a decision it already made. The same applies to an$IFDEFinside an$ELSEanywhere: use one flat top-level guard per platform, even though it repeats a little. mvpkg's test suite now fails the build on a nested one, having found two that had been working on three systems and broken on the fourth.
Have the error messages name MVMASTER too, so a failure says which file it
could not open.
UniData reads DIR as the file TYPE. jBASE reads it as the file NAME, and
leaves DIR and DIR]D in the account while the file you asked for never
appears. Nothing errors, so the failure surfaces later as a missing file.
jBASE catalogs per user, not per account: CATALOG BP FOO from an account
leaves that account's own bin/ and lib/ empty and writes $HOME/bin/FOO,
with subroutines going into a single shared $HOME/lib/lib0.so.
That sharing has a sharp edge. A program whose DEFC names a symbol the loader
cannot find does not fail on its own — it makes the shared library
unloadable, so every other cataloged program for that user stops working
too, with a message naming neither of them:
jBASE: /home/rocky/lib/lib0.so.314: undefined symbol: JBCURLGETFILE
** Error [ SUBROUTINE_CALL_FAIL ] **
Unable to perform CALL to subroutine CMD.INIT , Line 32 , Source MVPKG
That is MVPKG failing because an unrelated HTTP package was cataloged earlier in a different account. The symbol named is not the one being called, and the program named has nothing to do with the package at fault.
Two consequences worth designing around:
-
A package that adds DEFC entry points must ship its library and get it loaded — see the
LD_PRELOADsection above. Cataloging the BASIC half alone leaves a landmine for everything else that user runs. -
DELETE-CATALOGis the cure, and it rebuilds the shared library as it goes:Object HTTPGET decataloged successfully Library /home/rocky/lib/lib0.so.315 rebuild okayNote the version number moving: each catalog change writes a new
lib0.so.N, which is also why a staleLD_PRELOADor a long-lived session can be pinned to an older one.
Measured on jBASE 6.2.1.1 while installing mvpkg (mv_package#60): the install
was correct and MVPKG list still failed, because an earlier experiment had
left an unresolved symbol in the user's lib0.so.
JBCDEV_LIB and JBCDEV_BIN are UNSET by default, and the default is what you
want: CATALOG writes $HOME/lib/lib0.so.<n> and the runtime resolves against
it. Point them at an account-local lib/ and everything still compiles and
catalogs -- reporting success -- but every call fails at run time with
** Error [ SUBROUTINE_CALL_FAIL ] **
Unable to perform CALL to subroutine CMD.INIT
because nothing tells the loader about the new location.
Correction (measured on 6.2.1.1): the search path IS configurable — with
JBCOBJECTLIST. Setting JBCDEV_LIB alone moves the output and leaves the
loader looking in the old place, which is the failure above. Set both and it
works, for a cataloged SUBROUTINE as well as a verb:
# build time — where the catalog is written
JBCDEV_LIB=/opt/pkg/cmd/lib JBCDEV_BIN=/opt/pkg/cmd/lib CATALOG BP CMD.INIT
Library /opt/pkg/cmd/lib/lib0.so.1 rebuild okay
# run time — where the loader looks
JBCOBJECTLIST=/opt/pkg/cmd/lib ZCALLER
got: subroutine reached # without JBCOBJECTLIST: SUBROUTINE_CALL_FAILThat matters for more than tidiness. The default puts every package's catalog in
ONE $HOME/lib/lib0.so, where a single unresolvable DEFC symbol makes the
whole library unloadable and breaks every other cataloged program for that user
(see the section above). Giving each package its own JBCDEV_LIB directory
isolates that damage — the same shape MVX already has, where a package builds
LIB/libcmd.so of its own rather than sharing one.
The library inside the directory is still named lib0.so.<n>: what you get to
choose is the DIRECTORY, not the filename. Isolation is per directory, which is
enough.
Where to keep the search path: jBASE's own config, not a shell profile.
$JBCGLOBALDIR/config/jbase_config.json carries an environment array that
every session reads, with $VAR expansion — the same array PATH and
LD_LIBRARY_PATH are set in, and jBASE documents this variable there itself:
# JBCOBJECTLIST defines the directories to find user shared object libraries
# where user subroutines are located.
# {"name": "JBCOBJECTLIST", "default": "$HOME/lib"},
A profile in the account directory is not an option: jBASE reads nothing from
an account directory. A .profile dropped in one is ignored, and
JBCOBJECTLIST is empty by default — so anything kept there needs a human to
source it every session. On a stock install the config is group jbase and
group-writable, so an operator can maintain it without sudo.
Order matters, and $HOME/lib belongs LAST. jBASE searches the list in
order, so whatever sits earliest wins a name. $HOME/lib is where a tool
catalogs its own bootstrap copies of names that real packages later provide —
put it first and those cut-down copies shadow the real package for every program
on the system, permanently, while everything still appears to work.
Setting JBCDEV_LIB from BASIC: PUTENV("JBCDEV_LIB=" : DIR) works and the
EXECUTEd CATALOG inherits it, so a program can direct a catalog without
shelling out.
To compile from a tree outside the account — a package store, a staged
release — UniData and UniVerse need a temporary file pointer in the master
file. jBASE does not: BASIC and CATALOG both take a path directly, and
both take a wildcard over it:
BASIC /home/rocky/mvpkg/mvx-lang_getopt/BP * -> 11 compiled
CATALOG /home/rocky/mvpkg/mvx-lang_getopt/BP * -> 11 cataloged
And the pointer approach does not merely differ, it does not work: an MD
record of DIR/path/D_BP — the UniData spelling — fails to open, and so
does a Q pointer to the path.
Nor will SELECT enumerate such a directory. Opening the path succeeds and
then selects nothing — 0 items against 11 files — and jBASE's own CREATE-FILE ... TYPE=UD behaves the same. So list the directory with the shell when you
need the names, and hand the path to the verbs when you need the work done.
CREATE-ACCOUNT <path> -- the last element of the path becomes the account
name. It refuses a directory that is not empty unless given -f, and creates
bin/, lib/ and the MD. A bare directory with a BP in it is not an
account: COUNT MD answers "No file name could be found for your query".
There is no portable way to write "either of these symbols". Neither spelling works, and the two failure modes are not equally kind:
$IFDEF AA || BB |
$IFDEF AA OR BB |
|
|---|---|---|
| UniData | syntax error | syntax error |
| jBASE | compiles, reads AA, ignores the rest |
same |
| MVX | compiles, reads AA, ignores the rest |
same |
| UniVerse | silently false if the first symbol is undefined; compile error if it is defined | same |
UniData is the honest one: it refuses. jBASE and MVX take the first symbol and silently discard everything after it, which means the construct appears to work whenever the defined symbol happens to be written first.
UniVerse is the worst of the four, because it does BOTH depending on the input: with the first symbol undefined it skips the block quietly, and with the first symbol defined it enters the block and then chokes on the leftover tokens. The same line therefore compiles on one machine and fails on another according to which symbols happen to be set.
$DEFINE BB
$IFDEF BB || AA -> TRUE (looks like OR works)
$IFDEF AA || BB -> FALSE (same expression, reordered)
Both were tested by DEFINING ONLY THE SECOND SYMBOL and printing which branch ran -- compiling proves nothing here, because the broken cases compile.
Write nested guards, or $IFDEF the negative case, or put the fact in the
generated PLATFORM.H and test one symbol.
MVX will not be "fixed" to support this, deliberately. MVX is our own
compiler, so adding || there is possible -- and would be a trap: source using
it would compile correctly on MVX and be silently wrong on jBASE, or refuse to
compile at all on UniData. One symbol per $IFDEF is the portable subset, so
that is the subset MVX targets too. Being as capable as the least capable
system is the point.
This is the same trap one level deeper, and it is the more expensive one:
$IFDEF MVX
$DEFINE GETENV ENV
$ENDIF
V = GETENV("HOME")
On jBASE, where MVX is not defined, that still rewrites GETENV to ENV.
The compiler mentions it only as a warning -- "Variable ENV is never assigned"
-- so the program compiles, catalogs, and fails at RUN time with
Error in Conversion Code "HOME"
Unknown conversion code.
which names your variable as though it were a bad OCONV mask. GETENV itself
works perfectly well on jBASE; the guard around it was the bug.
The fix is not a better guard. Put the platform fact in the generated
PLATFORM.H, where only the platform that needs it gets a copy and there is no
$IFDEF to leak through. See MVMASTER above.
UniData's ! prefix hands the line to a shell. jBASE has no ! -- EXECUTE
reaches the OS directly, argv-style. There is no shell, so |, >, &&
and $? are not special, and quotes arrive as literal characters instead of
grouping anything. EXECUTE '!mkdir -p /x' fails as:
!mkdir: No such file or directory
sh -c "..." does not fix it, and it is the first thing everyone tries: the
quotes meant to hold the command together are passed through as text, so the
command arrives split across argv.
What works is to write the command to a script and run sh on the script:
SHSCRIPT = SHCMD
SHSCRIPT<-1> = "echo $? > " : SHR ;* exit status, which CAPTURING never gives
OSWRITE SHSCRIPT ON SHF
EXECUTE "sh " : SHF CAPTURING SHOUT
The file is the quoting boundary -- nothing re-parses the command text
between there and sh -- so pipelines, redirection, quotes and $? all behave
normally. The EXECUTE line carries a command and a path and nothing else.
Two traps inside that:
-
Do not redirect on the EXECUTE line.
EXECUTE "sh f > out 2>&1"hands jBASE>and2>&1as literal argv words -- the very problem being solved. Redirect inside the script, or take stdout throughCAPTURING. -
OSWRITE ... ON f ELSEis a syntax error on jBASE, as on UniData. NoELSEclause.
Getting the status back needs FIELD(rc, CHAR(10), 1), not TRIM(rc): echo
leaves a newline, TRIM strips only spaces, and NUM() then rejects the value
so every status reads as "unknown".
A directory is not an account until uv has set it up, and running uv there
asks two questions, not one. Piping only Y leaves you with no VOC and the
misleading impression that setup silently failed:
This directory is not set up for uniVerse.
Would you like to set it up (Y/N)? Y
0. Ideal UniVerse compatibility
1. IN2 compatibility ...
Which way do you wish to configure your VOC ?
So:
mkdir -p "$T" && cd "$T"
printf 'Y\n0\nQUIT\n' | "$UVHOME/bin/uv" # 0 = Ideal UniVerse
[ -e VOC ] || { echo "not provisioned"; exit 1; } # always assert it workedLet CREATE.FILE make its own directory. mkdir BP followed by
CREATE.FILE BP 19 fails -- the directory already exists -- and you are left
with a directory that is not a file, which then reports "Unable to open BP
file" from the compiler rather than from the thing that actually went wrong.
The same applies to UniData, where a pre-made MVPKG.INC leaves the VOC pointer
uncreated and $INCLUDE then cannot find it.
CREATE.FILE asks its seven questions whatever arguments you give it, and
there is no argument form that answers them. Every spelling that looks like it
should either mis-parses or leaves the DATA part unspecified:
>CREATE.FILE MVPKG.INC 19
Please enter the following information for the DATA file:
Modulo = <- the 19 was read as the DICT modulo
>CREATE.FILE DIR MVPKG.INC <- the UniData spelling
Improper modulo number entered.
Please enter the following information for the DICTionary file:
Modulo =
From TCL you can pipe the answers. From BASIC you cannot, and a question
asked inside EXECUTE is not an error — it is a hang. Put the answers on the
input stack instead:
DATA 1, 2, 3, 1, 2, 19, "" ;* dict 1/2/3, data 1/2/19, empty description
EXECUTE "CREATE.FILE MVPKG.INC" CAPTURING JUNK
CLEARDATAThree things about that line are load-bearing:
-
The empty seventh answer is the file description. Leave it out and the
question is asked; answer it with text and VOC attribute 1 becomes
F <description>(see below), which breaks anything matching on"F". -
Types 18 (hashed) and 19 (directory), never 30. UniVerse rejects 30,
re-prompts, and the re-prompt eats the rest of the stack — so the file is
never created and the leftovers are inherited by whatever
EXECUTEs next. That second failure lands a long way from its cause. -
CLEARDATAafterwards, for the same reason: a create that consumes fewer answers than it was handed leaves the rest on the stack.
Verified on UniVerse 14.2.1. It is idempotent — creating a file that already exists leaves the file, and its records, alone.
Reaching a file in another account, a Q-pointer's second attribute is an
account NAME, resolved through UD.ACCOUNT — so it only works for accounts that
registry knows:
MVPKG.STORE: :CT MVPKG.STORE installed
Q MVPKG.STORE is not a desired record in VOC file.
mvpkg :CT UD.ACCOUNT mvpkg
MVPKG.STORE UD.ACCOUNT does not exist in VOC file.
An account made by hand — newacct, or an installer that provisions its own —
is not in there, and the pointer names something unresolvable. A file pointer
carrying absolute paths needs no registry:
WRITE ("F" : @FM : P : "/MVPKG.STORE" : @FM : P : "/D_MVPKG.STORE") ON VOC, "MVPKG.STORE"Verified on UniData 8.3 and UniVerse 14.2.1. jBASE needs neither — it opens an absolute path as a file directly, which the other two refuse:
| platform | OPEN "/path/to/FILE" |
F pointer with paths |
|---|---|---|
| jBASE | works | — |
| UniData | fails | works |
| UniVerse | fails | works |
Same record, three attributes, different attribute 1 — and UniVerse does not accept the UniData spelling, so a hand-written pointer just fails to open:
| platform | attribute 1 |
|---|---|
| UniVerse | F |
| UniData, jBASE | DIR |
WRITE ("DIR" : @FM : SDIR : @FM : "D_BP") ON VOC, "MVPKG.SRC"
OPEN "MVPKG.SRC" TO F ELSE PRINT "cannot open" ;* <- taken, on UniVerseRead one of UniVerse's own back (CT VOC BP) and it says F. This matters
whenever code writes a temporary pointer to reach a directory outside the
account — a package store, a staged tree — because there CREATE.FILE is not
an option and the pointer has to be built by hand.
UniVerse has a global catalog, but its layout is not UniData's
$UDTHOME/sys/CTLG/<letter>/<name> and code that addresses that tree finds
nothing. The workable unit is the account: CATALOG <file> <prog> LOCAL FORCE registers the program in this account's VOC, which is what makes it both
typeable as a verb and CALLable as a subroutine.
Two consequences that both fail confusingly:
-
The catalog pointer is type
V, notC. UniData writesC; UniVerse's LOCAL catalog writesV, with the object path in attribute 2. A catalog-lookup helper reading only"C"answers "not cataloged" for every program on UniVerse — so an availability probe reports a working dependency as absent, and a package with a fallback bundles its own copy of something already installed. -
DEFFUN ... CALLING "*NAME"asks for the GLOBAL catalog. The leading star is not decoration; with LOCAL cataloging it fails at run time withProgram "MVPKG.SEARCH": Line 42, "*MVPKG.MAPFIELD" is not in the CATALOG space.even though
MVPKG.MAPFIELDis cataloged and callable. Drop the star —CALLING "MVPKG.MAPFIELD"resolves through the account's VOC. UniData needs the star, so this is a real per-platform difference and not a tidy-up:$IFDEF UDT DEFFUN MAPFIELD(A, B, C, D, E) CALLING "*MVPKG.MAPFIELD" $ENDIF $IFDEF UV DEFFUN MAPFIELD(A, B, C, D, E) CALLING "MVPKG.MAPFIELD" $ENDIF
Both are on the not-everywhere table below, but it is worth seeing what they look like, because neither error names the missing feature:
000027 OSREAD DEF FROM UDT : "/bin/work/cfuncdef" ELSE DEF = ""
^ Variable Name (UNDEFINED) unexpected, Was expecting: Assignment Operator
Array 'GETENV' never dimensioned.
UniVerse parses OSREAD as a variable being assigned, and GETENV(...) as a
subscripted array. Use OPENSEQ/READSEQ/WRITESEQ for the first; for the
second there is no built-in at all, so it has to come from the shell
(printenv NAME).
Both are worth routing through a single subroutine each rather than guarding at
every call site -- mvpkg has 42 OSREAD/OSWRITE and 20 GETENV uses, and
guarding those in place would mean 62 copies of the same two facts.
And when you do route them through one subroutine, decide what it returns:
OSREAD does not hand back the same thing on every port. See "OSREAD returns
the file's bytes on two systems and a dynamic array on two" in section 3 -- that
is the silent-difference half of this same statement.
In a FUNCTION, UniVerse requires the parenthesised form:
FUNCTION F(A)
RETURN(A : "-ok") <- compiles
RETURN A : "-ok" <- syntax error on UniVerse, fine everywhere else
The error is reported against the RETURN line and mentions neither functions
nor parentheses:
Variable Name (LOCAL) unexpected, Was expecting: ';', End of Line
RETURN(x) is accepted by MVX, UniData and jBASE too, so it is the portable
spelling -- always write the parentheses.
UniVerse has no such variable: it is a compile error there, which is at least loud. jBASE is the dangerous one. It compiles, and returns an EMPTY STRING:
X = @USER.TYPE
PRINT "len=":LEN(X):" isnum=":NUM(X):" ne0=":(X # 0)
len=0 isnum=1 ne0=1 <- jBASE 6.2.1.1
"" # 0 is true (measured the same on jBASE, UniData 8.3 and MVX), so the
standard guard
IF @USER.TYPE # 0 THEN RETURN ;* "not a terminal, stay quiet"returns ALWAYS on jBASE — the routine never runs, on any terminal, and nothing reports it. That is the exact opposite of the advice below, arrived at by accident.
Guard the check, not the program, and decide what "cannot ask" should mean: for an advisory message, assume interactive, because staying silent in a real terminal is worse than printing in a phantom.
What @USER.TYPE answers where it works (UniData 8.3, and MVX): 0 on a
real terminal, 1 in a phantom, 2 in a piped session. SYSTEM(2)/SYSTEM(3)
are no substitute — they returned the 80x23 defaults in all three contexts.
Note for test suites: a piped session answers 2, so a guard of this shape
returns immediately in any scripted test. A routine behind one has never been
exercised by a piped suite.
@LEVEL is how many programs are above this one. It compiles on every system
measured, which makes it look like the portable answer where @USER.TYPE is
not. It is not, and the way it fails is silent.
@LEVEL from the prompt |
through an EXECUTE
|
|
|---|---|---|
| UniData 8.3 | 0 | 1 |
| UniVerse 14.2.1 | 0 | 1 |
| jBASE 6.2.1.1 | 0 | 1 |
| ScarletDME (OpenQM) 2.6-6 | 1 | 2 |
| MVX | 0 | 1 |
So the idiom everyone writes
IF @LEVEL THEN RETURN ;* something is above me -- ask nobodyis true at the top level on OpenQM, and the routine returns always — the
same shape of silent failure as @USER.TYPE on jBASE, from the opposite
direction. Nothing reports it.
Portable code must not test @LEVEL for truth. Capture the base at entry and
compare against it, or test @LEVEL > BASE where BASE is established once:
COMMON /LVL/ BASE.LEVEL
IF BASE.LEVEL = "" THEN BASE.LEVEL = @LEVEL ;* whatever the top is here
IF @LEVEL > BASE.LEVEL THEN RETURNIt is also not a replacement for @USER.TYPE, whatever the base: in a
phantom it is still the top value (measured on UniData), so it cannot tell
you whether a terminal exists. The two are orthogonal, and a routine that
prompts wants both — and neither one ports without care.
A caller declaring
COMMON /CMDPKG/ CNAME, CDESC, CSUBS, CDESCS, CHANDLERS
against a subroutine declaring the same block with one more variable runs fine on jBASE and fails on UniVerse at RUN time:
Program "X": Line 1, COMMON size mismatch in subroutine "CMD.INIT".
Program "X": Line 1, Unable to load subroutine.
Which is the better behaviour -- but it means a mismatched declaration can sit in working jBASE code for a long time before UniVerse finds it. Keep the block in one include, or copy it exactly.
The UniVerse shell escape is SH -c, so the whole command becomes ONE argument
to sh and has to survive two parsers. Spelling that inline mixes ' and "
until neither balances, and the failure is silent:
EXECUTE "SH -c 'curl -sL -o " : TMP : ' "' : URL : "'" CAPTURING C
emits SH -c 'curl -sL -o /tmp/f "https://...' -- the double quote never
closes, curl sees no URL, writes nothing, and the caller gets "" with no error
raised anywhere. It is indistinguishable from an empty response.
Build the quotes from character codes so the nesting is explicit:
UDQ = CHAR(34) ; USQ = CHAR(39)
EXECUTE "SH -c " : UDQ : "curl -sL -o " : TMP : " " : USQ : URL : USQ : UDQ CAPTURING C
Better still, where the command is complex, write it to a script and run sh on
the file -- see the jBASE section above. The same reasoning applies: a command
that contains quotes cannot survive being wrapped in more quotes.
| name | where | what happens |
|---|---|---|
GT |
ECL (UniData) | reserved word; a program named BP/GT fails to compile with a misleading caret |
EQ |
MVX | relational operator — EQ = INDEX(...) is "expected statement"
|
SETTING |
UniData |
LOCATE ... SETTING; fails as a label, not only a variable |
These must be excluded by the preprocessor, not a runtime branch — the text still has to compile.
Guard on the positive platform ($IFDEF UDT), never on $IFNDEF MVX,
which is true on every other platform too.
| statement | exists on | trap elsewhere |
|---|---|---|
CALLC |
UniData | UniVerse parses R = CALLC F(x) as an array reference |
OSREAD / OSWRITE
|
UniData | UniVerse parses them as variables |
GETENV |
UniData | UniVerse has none — capture shell output instead |
KEYIN() |
MVX, UniVerse |
UniData has none, and a bare KEYIN compiles as an ordinary never-assigned variable |
CONVERT x TO y IN z |
UniData, UniVerse | not on MVX — the 3-argument function CONVERT(from,to,expr) works everywhere |
SELECT fvar TO 9 |
UniData | UniVerse rejects the numbered list: "option 'S' requires variable name". A select-list variable works on both |
MAT parameters |
UniVerse | UniData rejects them — "redefined variable ARGV"; MAT ARGV(8) in a signature is a syntax error. Only plain scalars are portable. jBASE accepts them but the dimension is real — it range-checks at compile time, so a signature's MAT A(1) rejects A(2) in the body |
DEFFUN f() CALLING "*NAME" |
UniData, UniVerse |
jBASE has no CALLING clause — syntax error near "CALLING". jBASE resolves a DEFFUN by name against the catalog, so where the name is already cataloged the aliasing is not needed; guard the clause and declare the plain DEFFUN
|
LOCATE ... SETTING (Format 2) |
flavour-dependent | use parenthesised Format 1 only; Format 2 searches a different level on UniData than UniVerse |
OSWRITE ... ELSE |
— | no ELSE clause on UniData: "Misuse of reserved word 'ELSE'", reported at a line well past the real one. OSREAD ... ELSE is fine |
TRANSACTION START/COMMIT/ABORT |
MVX, UniData, UniVerse | jBASE spells it TRANSTART / TRANSEND / TRANSABORT and rejects the U2 form with a bare syntax error under START
|
@TRANSACTION |
MVX, UniData, UniVerse |
jBASE has none — "Unknown @ system constant @TRANSACTION specified". jBASE asks the same question with TRANSQUERY()
|
The append form adds an element on UniData and jBASE. On UniVerse an EMPTY value is a no-op — the array does not grow — so two arrays maintained in parallel fall out of step the moment one of them takes an empty:
A<-1> = "one" ; B<-1> = "1.0"
A<-1> = "two" ; B<-1> = ""
A<-1> = "three" ; B<-1> = "3.0"| platform | DCOUNT(A) |
DCOUNT(B) |
B<2> |
|---|---|---|---|
| UniVerse | 3 | 2 | "3.0" |
| UniData | 3 | 3 | "" |
| jBASE | 3 | 3 | "" |
Nothing fails. Every read after the empty is simply off by one, and it stays that way in whatever the record is written to. It cost a package manager its version column: each package was paired with the NEXT one's version, so a freshly installed package reported as needing an upgrade.
Use an explicit index for parallel arrays, and the class disappears:
N = N + 1 ; C<N> = "two" ; D<N> = "" ;* both grow, alwaysThe same applies to <-1> on values and subvalues. If you must use the append
form, never let one of a parallel pair take an empty — push a sentinel instead.
The dangerous section. Nothing fails; you get a wrong answer.
Honoured on UniVerse, ignored on UniData, which returns the mask itself:
FMT(5,"R%4") gives "%4". # fills with spaces on both. There is no portable
zero-filled fixed-width spelling — pad by hand:
PADR = V:"" ; IF LEN(PADR) < W THEN PADR = STR("0", W-LEN(PADR)) : PADRHex-encodes a string on UniVerse; a no-op on UniData (there MX is
numeric-to-hex: OCONV(255,"MX") gives "FF").
Use "MX0C" — string-to-hex on both, byte-identical, CHAR(254) included,
and ICONV(...,"MX0C") round-trips with length preserved.
Both of these hit a length-framed wire protocol at once and produced no error anywhere: a framed wire has no resynchronisation point, so a short frame just makes the far end wait forever.
Inside one transaction, UniData 8.3 answers 1 (outside=0, inside=1).
UniVerse answers whatever the next transaction number happens to be — measured
on 14.2.1, the same one-START program answered 3, then 4, then 7, 8,
9 on successive runs:
PRINT "outside=":@TRANSACTION
TRANSACTION START ELSE PRINT "start failed"
PRINT "inside=":@TRANSACTION
TRANSACTION ABORToutside=0
inside=7 <- 8 on the next run, 9 on the one after
Nesting is counted on top of that number. Three successive STARTs on
UniVerse gave n, n+1, n+2, and each ABORT unwound one level, the last
returning to 0:
outside=0 after start1=4 after start2=5 after start3=6
after abort1=5 after abort2=4 after abort3=0
So IF @TRANSACTION = 1 compiles everywhere and silently never fires on
UniVerse — the worst possible shape, because the branch simply never runs
and no error is reported. Only truthiness ports:
IF @TRANSACTION THEN ... * portable
IF @TRANSACTION = 1 THEN ... * UniData onlyNesting differs too, and it is the same probe. UniVerse nests; UniData
refuses. Three STARTs on UniVerse all succeeded (above). On UniData 8.3 the
second one takes the ELSE and the depth does not move:
started
inside=1
nesting refused
after 2nd start=1
So a subroutine that opens its own transaction without checking gets a nested
one on UniVerse and a failed START on UniData. Have it check first, and only
commit what it started — this compiles and behaves identically on both:
MINE = 0
IF NOT(@TRANSACTION) THEN
TRANSACTION START ELSE STOP
MINE = 1
END
* ... do the work ...
IF MINE THEN
TRANSACTION COMMIT ELSE PRINT "commit failed"
ENDThe block IF ... END is not optional. The one-line spelling
IF MINE THEN TRANSACTION COMMIT ELSE PRINT "..." will not compile on
UniVerse: the ELSE binds to the IF, which leaves TRANSACTION COMMIT
without the clause UniVerse requires.
Called once outside a transaction and once inside one, that subroutine gives:
UniVerse: in sub: depth=10 mine=1 UniData: in sub: depth=1 mine=1
in sub: depth=11 mine=0 in sub: depth=1 mine=0
Same behaviour, different numbers — which is the whole point of the entry.
And UniVerse requires THEN/ELSE on TRANSACTION START; a bare one will
not compile:
000007 TRANSACTION START
^
End of Line unexpected, Was expecting: "ELSE", "ON", "THEN", "THENEOL", "ELSEEOL"
TRANSACTION ABORT, conversely, rejects a clause:
000002 TRANSACTION ABORT ELSE PRINT "aborted badly"
^
"ELSE" unexpected, Was expecting: ';', End of Line
Which is coherent — starting and committing can fail in ways a program should branch on, discarding cannot — but it means the three statements do not take the same shape, and a guard written for one will not compile for another.
True on both jBASE and UniData, which is what makes it dangerous — it is not a difference to guard, it is a shared behaviour nobody expects. The child runs outside the transaction and sees a stale view of anything the parent wrote inside it, with no error anywhere.
On jBASE 6.2.1.1, the parent holds a transaction and the child reports none:
parent TRANSQUERY after start=1
child TRANSQUERY=0
On UniData, a parent that writes a record inside a transaction and then
EXECUTEs a COUNT of that file gets 0 record(s) counted.
Keep anything that must see the transaction's own writes in-process — a
subroutine via CALL, not a program via EXECUTE.
While measuring that, two related facts on jBASE: an EXECUTE'd program gets a
fresh unnamed COMMON (reading it raises "Invalid or uninitialised
variable -- ZERO USED"), while a named COMMON block IS shared with the
caller. So COMMON /NAME/ crosses an EXECUTE and bare COMMON does not.
EXECUTE "LOGTO <account>" compiles everywhere. What it does afterwards is
the part that differs, and nothing reports the difference.
On UniData 8.3 and UniVerse 14.2.1 the calling program keeps running and stays in the new account:
PRINT "program: before the logto"
EXECUTE "LOGTO artdemo"
PRINT "program: after the logto"
EXECUTE "WHERE"program: before the logto
*** LOGIN PARAGRAPH RAN IN ARTDEMO ***
program: after the logto
/home/rocky/artdemo
Both lines print — so this is not a way to end a program — and the WHERE
afterwards reports the new account. TCL was still in artdemo when the
program ended. UniVerse behaves identically.
The target account's VOC LOGIN runs on the way in. With a PA
paragraph written to VOC as LOGIN:
1 PA
2 DISPLAY *** LOGIN PARAGRAPH RAN IN ARTDEMO ***
it ran in all three cases on both systems: a fresh login to the account, a
LOGTO typed at TCL, and the EXECUTE "LOGTO ..." above. So a program that
moves account triggers arbitrary code belonging to that account — worth
knowing before a menu moves between companies.
A handle opened before the LOGTO still reads afterwards, on UniData and
UniVerse — a handle there names a file, not a store. Measured against a file
that exists only in the account being left, so it is the handle surviving
and not a file of the same name in the new account:
opened LTONLY in demo
/home/rocky/artdemo
old handle reads: written in demo
and LTONLY does NOT exist in artdemo (control)
| opened | after the LOGTO
|
control | |
|---|---|---|---|
| UniData 8.3 | demo/LTONLY |
reads written in demo
|
not in artdemo |
| UniVerse 14.2.1 | bench/LTONLY |
reads written in bench
|
not in ltacct |
Use a file unique to one account when checking this yourself: an earlier run of this probe opened a file both accounts had, and could not tell a surviving handle from a same-named file in the new account.
Where it bites when porting to MVX: MVX has no LOGIN hook at all, so
account setup that lived in a LOGIN paragraph has to move into the program.
The move itself works — EXECUTE "LOGTO ...", or the native LOGTO()
intrinsic, which returns 1/0 with the reason in STATUS() — but MVX closes
the old account's files and refuses the move while a transaction is open.
Code that keeps handles in COMMON across a LOGTO — the Gentrack pattern —
must re-open them on MVX; a stale handle there says so rather than reading
whatever the old one pointed at. The difference is not stubbornness: an MVX
handle names a place in a store reached through the account's bindings, not a
path to a file, so one that survived would follow the session into the new
account rather than keep pointing where it was opened.
Both systems run something when you enter an account — a fresh login, a
LOGTO from TCL, and a LOGTO from inside a BASIC program. What it is
called differs, and nothing reports the mismatch.
PA paragraphs written into VOC, each printing a distinct banner:
| record | UniData 8.3 | UniVerse 14.2.1 | jBASE 6.2.1.1 | ScarletDME 2.6-6 |
|---|---|---|---|---|
LOGIN |
runs | runs | nothing | runs |
the account name (demo, bench, lgtest, qmtest) |
ignored |
runs — matching the account name exactly as the system knows it, so lowercase bench; BENCH never fired |
nothing | ignored |
the Unix user name (rocky / ROCKY) |
ignored | — | — | — |
LOGIN and the account name both present |
LOGIN runs |
the account name wins; LOGIN does not run |
— |
LOGIN runs |
Paragraphs (PA) and PROCs (PQ) both run on ScarletDME, so a LOGIN there
may be either, as on the U2 systems.
Use LOGIN. It is the only spelling that works on both U2 systems.
jBASE has none of this, because its login IS the Unix login. Entering an
account runs nothing: a PA record written into MD is not recognised —
invoking it by name falls through to the OS (LOGIN: No such file or directory, because jsh runs OS commands) — and that holds under the default
emulation and under -e d3. jBASE MD verb records are opcode entries
(001 t, 002 16), not paths or paragraphs.
An account there is a directory registered in $JBCDATADIR/SYSTEM:
lgx
001 D <- type: directory
002 /home/rocky/lgx <- path
003 (empty) ...
There is no VOC. You log into Unix, your shell profile sets the jBASE
environment and starts jsh in an account directory — so the .profile is
the login proc, and it lives outside the account. Nothing an account carries
can run on entry.
Its LOGTO is also a genuine re-authentication: it prompts for a password
(logto.b), where the U2 LOGTO just moves.
Unverified: the jsh binary carries three login-shaped hooks — a sourced
shell script $JBCRELEASEDIR/config/jshlogin, a JBCLOGINPROG environment
variable, and a LoginProcAttr1 symbol. On 6.2.1.1 none of them fired
when tried: creating config/jshlogin, setting JBCLOGINPROG to a cataloged
program, running jsh as a login shell (argv[0] = -jsh), and putting a
verb in SYSTEM attribute 3 all did nothing. They may need a condition not
reproduced here, or be vestigial. Do not rely on them without testing.
The trap runs one way: on UniVerse an account that already has an
account-named record keeps using it and silently ignores a LOGIN added
beside it. If a LOGIN you installed appears to do nothing on UniVerse, look
for a VOC record named after the account.
An account-named login does not survive a rename, which matters when a
whole account is moved or checked out under a different name — the login
simply stops existing. Normalise it to LOGIN when moving an account between
systems.
Not a precision difference — a unit difference, so elapsed-time arithmetic is wrong by a factor of 1000 with nothing reported. Measured at roughly the same moment of the day:
TIME() |
SYSTEM(12) |
|
|---|---|---|
| MVX | 48383 |
48383240 (ms) |
| jBASE 6.2.1.1 | 12368 |
12368816 (ms) |
| UniData 8.3 | 12458 |
12458050 (ms) |
| UniVerse 14.2.1 | 12229.8222 |
12229.8222 (s, fractional) |
On UniVerse SYSTEM(12) is simply TIME() again, fraction included — it is
the only one of the four that is not milliseconds.
TIME() carrying a fraction on UniVerse also breaks the standard "wait for the
clock to tick" calibration loop, because TIME() # START is true immediately:
START = TIME() ; LOOP UNTIL TIME() # START DO REPEAT * never waits on UV
START = INT(TIME()) ; LOOP UNTIL INT(TIME()) # START DO REPEAT * portableLeft unguarded it does not fail — it reports. A benchmark calibrated this way on UniVerse claimed a run of 203,804 seconds.
Reading a whole OS file gives you two different things depending on the port, and both compile and both look right:
| how a file is read | a two-line file AAA\nBBB\n
|
|
|---|---|---|
| MVX |
OSREAD() function |
raw bytes — DCOUNT(nl)=3 DCOUNT(@AM)=1
|
| UniData | OSREAD ... FROM |
raw bytes — DCOUNT(nl)=3 DCOUNT(@AM)=1
|
| jBASE | OSREAD ... FROM |
newline → @AM — DCOUNT(nl)=1 DCOUNT(@AM)=2
|
| UniVerse | no OSREAD; READSEQ line by line |
whatever you join the lines with |
So the same source line
OPATH = TRIM(FIELD(OINFO, CHAR(10), 2))returns the second line on MVX and UniData, and the empty string on jBASE and UniVerse. No error, on any of them.
jBASE also drops the trailing newline, so a file ending in one reads back a
byte shorter there than on UniData — which means DCOUNT(x, CHAR(10)) differs
by one between ports even after you have normalised the separator. Parse with
FIELD, never by counting to the end.
In mv_package#118 this made MVPKG.FILE("READ", ...) mean three things at once:
its own header documented @AM, every one of its callers parsed CHAR(10), and
only two of the four ports produced @AM. The store's operator marker parsed
to an empty owner path, a VOC file pointer was written to /MVPKG.STORE
instead of /home/rocky/uv98d/mvpkg/MVPKG.STORE, and MVPKG list reported
mvpkg: cannot open the package store MVPKG.STORE
on a fresh install that reported success at every step. The parallel
STOREWRITE failed the same way and silently, so installs never reached the
inventory either.
Normalise in the seam, and say which form it hands back. A seam whose
documented contract nothing implements is worse than no seam: it makes the
callers look wrong. If you convert on jBASE, use the CONVERT statement —
the CONVERT() function takes its arguments in the opposite order there (see
§3 above) and returns rubbish rather than failing.
BASIC <file> <item> <item> ... takes a list of items to compile. On UniData
8.3 a list of 26 or more crashes the session with SIGSEGV — after compiling
every one of them successfully, so the output reads like a clean run right up to
the moment it dies:
item id =GIT.AGENT
Compiling Unibasic: .../BP/GIT.AGENT in mode 'u'.
compilation finished
Segmentation fault
It is the count, not the length. Measured with synthetic programs so the name length is controlled:
| items | name length | list bytes | result |
|---|---|---|---|
| 26 | 4 | 130 | SIGSEGV |
| 26 | 21 | 572 | SIGSEGV |
| 25 | 21 | 550 | clean |
130 bytes crashes where 550 bytes passes, so there is no command-line limit to work around by shortening names — the list has to be shorter than 26. Nor is it one bad program: two disjoint 26-item sets crash identically and both 25-item sets pass. UniVerse has no such limit and compiled the same 59 items in one command.
Compile in batches (mv_package uses 20, leaving room for the ceiling to be lower on another release). In mv_package#125 this made a 59-program package uninstallable on any UniData machine that did not already have its objects built.
Two things make this hard to see, and both are worth knowing on their own:
- A crashed install leaves its objects behind, so the next attempt compiles nothing and succeeds. The bug reads as intermittent, and a control run measures nothing unless the built package is deleted by hand first.
-
Each SIGSEGV strands a UniData session holding a licence slot. On a 2-user
Trial Edition, two crashes lock the system out with
Licensed # of users has been reached— which looks nothing like the original fault. Clear them withlistuser+deleteuser -f <pid>, and note thatlistuserwrites its session table to STDERR: a2>/dev/nullsilently discards exactly the rows needed to clean up, and the cleanup then appears to run and do nothing.
UniVerse writes attribute 1 as "F " — with a trailing space — when the
CREATE.FILE description is empty, and appends the description when there is
one, so a described file reads "F Customer master".
Anything matching attribute 1 must take the first TOKEN. Matching the whole
field made mv_voc_class("F ", 2) answer "not a file" for every file in the
account, and every downstream rule silently stopped firing.
| platform | object |
|---|---|
| UniVerse | a separate file named for the source file: BP → BP.O
|
| UniData |
inside the source file, as _<PROG> beside <PROG>
|
| jBASE |
inside the source file too, but as $<PROG>
|
| MVX |
CATALOG/, which is not a record file |
The two prefixes bite the same way and in shell as well as BASIC: a loop over
BP/* that does not skip them finds twice as many "programs" after the first
compile and tries to build _MVPKGOS or $MVPKGOS. In sh, note that $* in
a case pattern is the positional parameters — the literal needs escaping:
case "$f" in (\$*|.*) continue ;; esacAnything walking an account's records needs all three rules, because a repository is shared between systems.
On UniVerse the .O directory must already exist. Compiling from a file
that points outside the account — a package store, a staged tree — the compiler
creates the pointer to the object file and then stops, because the OS
directory behind it is not there:
Creating file "MVPKG.SRC.O" as a remote pointer to operating system file "/store/pkg/BP.O".
The remote operating system file "/store/pkg/BP.O" does not exist.
[ENOENT] No such file or directory
So mkdir -p <dir>.O before compiling. UniData needs nothing equivalent: its
objects go inside the source file as _<PROG>.
And the pointer comes in a pair. Compiling through a file pointer you wrote yourself makes UniVerse create a SECOND VOC record for the object side:
Creating file "MVPKG.SRC.O" as a remote pointer to operating system file
"/store/pkg/BP.O".
It survives, and nothing you wrote knows about it. Re-point the original at a
different directory and the compiler still writes to the old one, because the
.O record was never updated — so a loop that compiles several trees through one
temporary pointer puts every object in the FIRST tree, and catalogs every program
against it. Measured across five packages: only the first had any objects, and
every catalog entry named it.
Delete <name>.O alongside <name> whenever you create or drop a temporary
pointer.
-
The master file is
MD, notVOC— the first platform to disagree. -
MDis theMD]Dfile:jstat MDreportsFile …/MD]D, andCT MD/CT DICT MDreturn the same records. The master file is dictionary-only, so noMDdata file exists on disk — code that skips]Dnames as dictionaries makes the master file invisible. -
CREATE-ACCOUNTregisters the name in/jbasedata/SYSTEM]Dand refuses a name already there.DELETE-ACCOUNT -f <name>clears it — and deletes the directory it points at, so guard it. -
CREATE-FILEwrites no MD record.SET-FILE <acct> <file> <name>does, and takes the account name, not a path. - A fresh account's MD holds 242 stock records; the template is
$JBCRELEASEDIR/src/MD]D.
-
Every BASIC source file needs a trailing newline or the compiler reports "End of File unexpected" at the last line.
-
CREATE.FILEasks seven questions whatever arguments you pass, and the seventh is the file description — leave it empty (see attribute 1, above). From BASIC, stack the answers withDATA(see above); there is no argument form that avoids the questions. -
Cataloging is per account.
CATALOG <file> <prog> LOCAL FORCEis the unit that works; the pointer it leaves is VOC typeV, andCALLING "*NAME"will not find it. -
A directory that is not a VOC-registered file is invisible to
BASIC, which reports it by compiling nothing rather than by failing. -
Sockets are TCP-only; FIFOs carry binary if length-framed, and
READBLKmust never over-request. -
Every bare
RETURNinside aFUNCTIONwarns, and you cannot silence it. AGOSUBsection has to end inRETURN, and the compiler cannot tell that from a function return, so it says:000093 RETURN ^ WARNING: no RETURN value specified, null used.The runtime is correct — the GOSUB returns to its caller and the function still returns its own value. Measured, rather than assumed:
FUNCTION ZGTEST(X) MARK = "start" GOSUB SUB1 MARK := "|after-gosub" RETURN("done:" : MARK) SUB1: MARK := "|in-sub" RETURNresult=[done:start|in-sub|after-gosub]And
RETURN ""is not the way to quiet it — it is a compile error:WARNING: no RETURN value specified, null used. ^ String Constant unexpected, Was expecting: ';', End of Line 1 Errors detected, No Object Code Produced.So the bare
RETURNis the only form that compiles, and the warning is unavoidable noise. UniData says nothing about the same source. Do not restructure working code to chase it: count the warnings a file already produces, and look only at a change in the count.
-
$INCLUDEresolution needsCREATE.FILE DIR BP.INC, notCREATE.FILE BP.INC 19. - CallC gives C no file API, so a C extension cannot read records: BASIC drives the I/O and C does the work.
These produce failures that point nowhere near their cause. Check them before believing a mass failure is a code regression.
A system-wide catalog makes a failed install look like a working one.
UniData's catalog is $UDTHOME/sys/CTLG, shared by every account on the
machine, and jBASE's is ~/lib, shared by every account of that user. So an
install that compiles NOTHING still leaves a runnable account, resolving
against whatever an earlier install left there — for months, in one case.
The check that missed it asked whether the object exists:
ls "$UDTHOME"/sys/CTLG/*/MVPKGOS >/dev/null 2>&1 || die "catalog failed"It cannot fail on a machine that has ever installed the package. Ask instead whether this run produced it:
MARK=$(mktemp) # before the catalog step
...
find "$UDTHOME"/sys/CTLG/*/MVPKGOS -newer "$MARK" | head -1The same applies to the compile: udt and jsh both report a failed compile on
stdout and still exit 0, so the exit status is not evidence and the output
is. It surfaced only when a newly added program had no stale entry to fall back
on — which is the general shape of it: shared state hides a broken install until
something genuinely new is asked for.
-
TERMmust be set (export TERM=vt100) on UniData and UniVerse. A bare non-interactive shell has noTERM, verbs produce empty output, and a whole suite reads as broken. Nothing in the failure mentionsTERM. -
Licence seats are few. UniData TE licenses 2 concurrent sessions;
UniVerse TE has 2 seats (
uvlictool). A suite faster than seats are released transiently wants a third, and the symptom is never the same twice: an explicit "user limit has been reached", or empty output, or the next command reporting a cold session's login banner. Report seats before a run — an unreported licence makes every failure below it misleading. -
A dead session can hold a slot. UniData leaves a phantom
listuserentry with no process; clear it withdeleteuser. - Stale artefacts answer for the new ones. A rebuilt package does not change what runs if the installed binary is what the test invokes; a shared library assembled from two fragments staging the same object silently keeps one of them. Both have shipped month-old code while the install reported success.
It is a flag for $IFDEF, not a macro with a value. All three of these fail
to compile on UniData 8.3:
$DEFINE MVMASTER "VOC" * syntax error ... missing quote "
$DEFINE MVMASTER 'VOC' * syntax error ... missing quote '
$DEFINE MVMASTER VOC * syntax error at the $INCLUDE lineThe error is reported against the including line, not the $DEFINE, so it
points at the wrong file:
main program: syntax error at or before
<line 1> $INCLUDE TSTINC PLATB.H
jBASE and MVX both accept a valued define and substitute it — verified on
6.2.1.1 and on MVX, where $DEFINE MVMASTER "MD" then CRT MVMASTER prints
MD. So this compiles on two platforms and fails on a third, which is the
worst way to find out.
To give BASIC a per-platform value, generate it into the per-platform
PLATFORM.H as an ordinary assignment instead:
MVMASTER = "MD" * written by build-jbase.sh
MVMASTER = "VOC" * written by the othersEvery source $INCLUDEs PLATFORM.H immediately after its SUBROUTINE line,
so an assignment there is legal and becomes the first executable statement.
-
$IFDEFinside a comment still counts. Both of these compile clean on UniData and fail on UniVerse. -
$ELSE-nested guards are not portable — flatten them.
When a port turns up a difference, add it here with the evidence — the source that failed and the exact message — not a summary. The message is what the next person will search for.
Put it in the section that matches how it fails, since that is what decides how hard it is to find: refuses to compile, compiles and misbehaves, or compiles everywhere and differs silently.