SPORADIC DIALOGUES
                       by Eugene Volokh, VESOFT
        Q&A column published in INTERACT Magazine, 1987-1989.
   Published in "Thoughts & Discourses on HP3000 Software", 4th ed.


Q:  I have what you might think to be a strange question --  where  is
the Command Interpreter?

On  my   MPE/XL  machine, I see a program file called CI.PUB.SYS, so I
guess  that that's the  Command  Interpreter  program  (the  one  that
implements   :RUN,  :PURGE,  etc.)  -- however, I couldn't find such a
program on my MPE/V computer.

:EDITOR,  I know, is implemented  through  EDITOR.PUB.SYS;  :FCOPY  is
FCOPY.PUB.SYS;  all the compilers have their own program files.  Where
is the CI kept?

A:  This  is a very interesting question -- one that bothered me for a
while several years ago.  The answer is somewhat surprising.

Virtually  all of the operating system on MPE/V is kept in the  system
SL   (SL.PUB.SYS).  This includes the file system, the memory manager,
etc.;  it also includes the Command Interpreter.  But,  you  may  say,
each   job  or  session  has  a  Command Interpreter process, and each
process  must have a program file attached to it.  Nope.  Each of YOUR
processes  must have a program file attached to it; MPE  itself  faces
no such restrictions.

All  that   a  process really has to have is a data segment (which MPE
builds  for each CI process when  you  sign  on)  and  code  segments;
internally,  there's no reason why those code segments can't be in the
system   SL.   MPE  just uses an internal CREATEPROCESS-like procedure
that  creates a process given not a program file name but  an  address
(plabel) of a system SL procedure; no program file needed.

On  MPE/XL,   the situation is somewhat different (as you've noticed);
there's  a program file called CI.PUB.SYS that is actually what is run
for  each CI process.  However,  if  you  do  a  :LISTF  CI.PUB.SYS,2,
you'll   find that it's actually quite small (186 records on my MPE/XL
1.1  system); since Native Mode program files usually take a lot  more
records  than MPE/V program files, those 186 records can't really do a
lot   of  work.   CI.PUB.SYS  is  really just a shell that outputs the
prompt,  reads the input, and executes it  by  calling  MPE/XL  system
library   procedures.   MPE/XL  library procedures (including the ones
that  CI.PUB.SYS calls, either directly, or indirectly)  are  kept  in
the files NL.PUB.SYS, XL.PUB.SYS, and SL.PUB.SYS.

Thus,  in   both  MPE/V and MPE/XL, virtually all of the smarts behind
command  execution is kept  in  the  system  library  (or  libraries).
Notable   exceptions include all the "process-handling" commands, like
EDITOR,  FCOPY, STORE/RESTORE (which use the  program  STORE.PUB.SYS),
and    even  PREP  (which  actually  goes  through  a  program  called
SEGPROC.PUB.SYS).   However, :FILE, :PURGE, :BUILD, and so on, are all
handled by SL, XL, or NL code.


Q:  A couple of months ago I read in this column that you can speed up
access  to databases by having somebody  else  open  them  first.   Is
there   some easy way that I can take advantage of this without having
to  write a special program?  I guess what I'd have to do  is  have  a
job   that  DBOPENs  the  database and then suspends with the database
open, but how can this be easily done?

A:  Indeed, if a TurboIMAGE database is already DBOPENed  by  somebody
then   a  subsequent DBOPEN will be faster than it would be if it were
the  first DBOPEN.  On my Micro XE, a DBOPEN of a database that nobody
else  has open took about 2 seconds; a DBOPEN of a database  that  was
already   opened  by  somebody  else took only about 0.8 seconds.  The
first  DBGET/DBPUT/DBUPDATE against a dataset were also faster if  the
database   was  already open (for the DBGET, about 50 milliseconds vs.
about 400-500 milliseconds).

How  can you take  advantage  of  this?   Well,  if  the  database  is
constantly  in use (e.g. it's opened by each of the many users that is
accessing   your  application  system),  then  the  time savings comes
automatically  -- the first user will spend 2 seconds  on  the  DBOPEN
and   all  subsequent  users  will  spend  0.8 seconds, as long as the
database is open by at least one other user.

The  problem arises when your database is  frequently  opened  but  is
left   open for only a short time; for instance, if it is used by some
short  program that only needs  to  do  a  few  database  transactions
before  it terminates.  Then, you might have hundreds of DBOPENs every
day,   the  great  majority  of  which happen when the database is NOT
already  open.  You'd like to have one job hanging out  there  keeping
the   database  open  so  that  all  subsequent  DBOPENs will find the
database already opened by that job.

How  can this be done without writing a special  custom  program?   (I
agree   that  you should try to avoid writing custom programs whenever
possible  -- if you wrote a special program, you'd have to keep  track
of   three  files [the job stream, the program, and the source] rather
than just the job stream.) Well, let's take this one step at a time.

Having a job stream that opens your database is easy -- just say

   !JOB OPENBASE,...
   !RUN QUERY.PUB.SYS
   BASE=MYBASE
   <<password>>
   1   <<the mode>>

Unfortunately,  as soon as this job opens the database, it'll start to
execute  all the subsequent QUERY commands; eventually (because of  an
>EXIT   or  an end-of-file) QUERY will terminate and the database will
get  closed.  It would be nice  if  QUERY  had  some  sort  of  >PAUSE
command   (or,  to  be  precise,  a  >PAUSE  FOREVER  command), but it
doesn't.

Or  does  it?   What  are  the  mechanisms  that  MPE  gives  you  for
suspending a process?  Can we use any of them from within QUERY?

Well,  there's obviously the PAUSE intrinsic, which pauses for a given
amount   of time.  It's not quite what we want (since we want to pause
indefinitely),  but more importantly there is really no way of calling
PAUSE  from within QUERY (unless your QUERY is  hooked  with  VESOFT's
MPEX).   So much for that idea.  There are also a few other intrinsics
--   for instance, SUSPEND and PRINTOPREPLY -- that suspend a process,
but   they're  not  callable  from  QUERY  either.   You  can't   call
intrinsics   from QUERY; you can't even do MPE commands (though in any
event there aren't any MPE commands that suspend a process).

One  other thing, however, comes to mind --  message  files.   A  read
against   an  empty  message  file is a good way to suspend a process;
however,  QUERY doesn't have  an  ">FOPEN"  or  an  ">FREAD"  command,
either, does it?

Well,  in  a way it does.  QUERY's >XEQ command lets you execute QUERY
commands  from an external MPE file, and to do that it has to FOPEN it
and FREAD from it.  You might therefore say:

   !JOB OPENBASE,...
   !BUILD TEMPMSG;MSG;TEMP;REC=,,,ASCII
   !RUN QUERY.PUB.SYS
   BASE=MYBASE
   <<password>>
   1   <<the mode>>
   XEQ TEMPMSG
   EXIT
   !EOJ

The  XEQ will start reading from this temporary message file, see that
it's  empty, and hang, waiting for a record  to  be  written  to  this
file.    Since  the file is a temporary file, nobody else will be able
to write to it, so the job will remain suspended until it's aborted.

There  are a few other alternatives along the same  lines.   For  one,
you   could  make the message file permanent -- that way, you can stop
the  job by just writing a record to this file.  Why would you want do
this?   Because you may want to have  your  backup  job  automatically
abort the OPENBASE job so that the database can get backed up.

If  the   message file were permanent, your backup job could then just
say:

   !FCOPY FROM;TO=OPENMSG.JOB.SYS
   << just a blank line -- any text will do >>
   ...

The  one thing that you'd have to watch out for is the security on the
OPENMSG.JOB.SYS  file -- you wouldn't want to  have  just  anybody  be
able   to  write to this file because any commands that are written to
the  OPENMSG file will be executed by the QUERY in  the  OPENBASE  job
stream.  (Remember the >XEQ command.)

If  you   don't use the permanent message file approach, you can still
have the background job abort the OPENBASE job by saying

   !ABORTJOB LOCKBASE,user.account

However,  for this, you'd either have to  have  :JOBSECURITY  LOW  and
have  your backup job log on with SM capability, or have the :ABORTJOB
command   be globally allowed (unless you have the contributed ALLOWME
program or SECURITY/3000's $ALLOW feature).

Another  alternative is to >XEQ a file that requires a  :REPLY  rather
than a message file, e.g.

   !JOB OPENBASE,...
   !FILE NOREPLY;DEV=TAPE
   !RUN QUERY.PUB.SYS
   BASE=MYBASE
   <<password>>
   1   <<the mode>>
   XEQ *NOREPLY
   EXIT
   !EOJ

The  trouble with this solution is that  the  console  operator  might
then   accidentally  :REPLY  to the "NOREPLY" request.  (Also, the job
wouldn't quite work if you had an auto-:REPLY tape drive!)

In  any event, though, one of the above solutions might be  the  thing
for  you.  It's not going to buy you too much time (it only saves time
for   DBOPENs  and the first DBGETs/DBPUTs/DBUPDATEs on each dataset),
it  isn't necessary if the database is already likely to be opened all
the  time, and it won't work  on  pre-TurboIMAGE  or  MPE/XL  systems.
However,   in  some  cases  it  could  be  a  non-trivial (and cheap!)
performance improvement.

One  note to keep in mind (if you really look  carefully  at  the  way
IMAGE   behaves):   the  BASE=  in  the  job stream will only open the
database  root file -- the individual  datasets  will  remain  closed.
HOWEVER,   once the keep-the-database-open job starts, any open of any
dataset  by any user will leave the dataset open for all  the  others;
even   when  the  user  closes  the  database, the datasets will still
remain  open.  If the user only reads the dataset (and  doesn't  write
to   it),  the  dataset  will only be opened for read access; however,
once  any user tries to write to the  dataset,  the  dataset  will  be
re-opened for both read and write access.

The  upshot of this is that the database will start out with only  the
root   file opened, and then (as users start accessing datasets in it)
will  slowly have more and more of its datasets  opened.   After  each
dataset   has  been accessed (especially written to) at least once, no
more  opens will be necessary until the  last  user  of  the  database
(most likely the keep-open job itself) closes it.


Q:  I recently tried to develop a  way to periodically check my system
disk  usage  (free,  used,  and lost). I based  it on information that
VESOFT once supplied in one of their articles.

My procedure was as follows:

   1.  Do  a  :REPORT  XXXXXXXX.@ into a file  to determine total disk
      usage by account.

   2.  Subtotal the list (in my case, using QUIZ) to get a system disk
      space usage total.

   3. Run FREE5.PUB.SYS with ;STDLIST= redirected to a disk file.

   4.  Use QUIZ to combine the two output files and convert the totals
      to Megabytes.

This  way, I can show the total free space and total used space on one
report for easy examination. I can also add these two numbers, compare
the sum to the total disk capacity, and thus determine the 'lost' disk
space.

My  question is in regard to the lost disk space. The first time I ran
the  job,  the  total lost disk space came  out to be approximately 19
Megabytes. After doing a Coolstart with 'Recover Lost disk Space', the
job  again showed a total lost disk  space of 19 Megabytes. Didn't the
Recover  Lost disk Space save any  space? Could there be something I'm
overlooking?

A: Unfortunately, there is. Paradoxical as it may seem, the sum of the
total  used  space and the total free space  is NOT supposed to be the
same  as the total disk space on  the system; or, to be precise, there
is more "used space" on your system than just what the :REPORT command
shows you.

The  :REPORT  command shows you the  total space occupied by PERMANENT
disk FILES. However, other objects on the system also use disk space:

   - TEMPORARY FILES created by various jobs and sessions;

   - "NEW" FILES, i.e. disk files that are opened by various processes
     but not saved as either permanent or temporary;

   -  SPOOL  FILES,  both  output  and  input  (input spool  files are
     typically the $STDINs of jobs);

   - THE SYSTEM DIRECTORY;

   - VIRTUAL MEMORY;

   -  and various other objects, mostly  very small ones (such as disk
     labels, defective tracks, etc.).

Your job stream, for instance, certainly has a $STDLIST spool file and
a  $STDIN file (both of which use disk space), and might use temporary
files  (for instance, for the output  of the :REPORT and FREE5); also,
if  you weren't the only user on the  system, any of the other jobs or
sessions might have had temporary files, new files, or spool files.

In  other words, to get really precise "lost space" results, you ought
to:

   * Change  your  job  stream to take into  account input and output
     spool  file  space  (available  from  the  :SHOWIN  JOB=@;SP  and
     :SHOWOUT  JOB=@;SP  commands). Since :SHOWIN  and :SHOWOUT output
     can  not  normally  be  redirected  to  a  disk file,  you should
     accomplish  this  by  running  a  program  (say, FCOPY)  with its
     ;STDLIST=  redirected and then making  the program do the :SHOWIN
     and :SHOWOUT, e.g. by saying

       :RUN FCOPY.PUB.SYS;INFO=":SHOWOUT JOB=@;SP";STDLIST=...

   * Consider  the space used by the  system directory and by virtual
     memory (these values are available using a :SYSDUMP $NULL).

   * Consider the space used by your own job's temporary files.

   * Run the job when you're the only user on the system.

Your best bet would probably be to run the job once, immediately after
a  recover lost disk space, with nobody  else on the system; the total
'unaccounted  for' disk space it shows you (i.e. the total space minus
the :REPORT sum minus the free disk space) will be, by definition, the
amount  of  space  need by the system and  by your job stream. You can
call    this   post-recover-lost-disk-space   value   the   'baseline'
unaccounted-for total.

If  your job stream ever shows you  an 'unaccounted for' total that is
greater  than  the  baseline, you'll know that  there MAY be some disk
space lost. To be sure, you should

   * Run the job when you're the only user on the system.

   * Make sure that your session (the  only one on the system) has no
     temporary files or open new files while the job is running.

If  you  do all this, then comparing  the 'unaccounted for' disk space
total against the baseline will tell you just how much space is really
lost.

Incidentally, note that you needn't rely on your guess as to the total
size  of your disks -- even this  can be found out exactly (though not
easily).  The  very end of the  output of VINIT's ">PFSPACE ldev;ADDR"
command  shows  the  total  disk size as  the "TOTAL VOLUME CAPACITY".
Thus,  you could, in your job,  :RUN PVINIT.PUB.SYS (the VINIT program
file)  with ;STDLIST= redirected to a  disk file and issue a ">PFSPACE
ldev;ADDR"  command for each of your disk  drives. (If you want to get
REALLY  general-purpose,  you  can  even  avoid  relying on  the exact
logical device numbers of your disks by doing a :DSTAT ALL into a disk
file  and  then  converting  the output of this  command into input to
VINIT).

Finally,  it's  important  to  remember that all  this applies ONLY to
MPE/V.  The rules of the game for  MPE/XL are very, very different; in
any event, disk space on MPE/XL should (supposedly) never get lost.


Q:  I  want  to "edit" a spool file --  make a few modifications to it
before  printing  it. SPOOK, of course, doesn't  let you do this, so I
decided  just to use SPOOK's >COPY command  to copy the file to a disk
file,  use  EDITOR to edit it, and  then print the disk file. However,
when  I  printed  the  file,  the  output  pagination  ended up  a lot
different  from  the  way it was in the  original spool file! Is there
some  special  way  in  which  I should print the  file, or am I doing
something else wrong?

A:  Well, first of all, there is a special way in which you must print
any  file that used to have Carriage Control before you edited it with
EDITOR.  When  EDITOR  /TEXTs in a CCTL  file, it conveniently forgets
that  the file had CCTL -- when it /KEEPs the file back, the file ends
up  being a non-CCTL file (although  the carriage control codes remain
as data in the first column of the file). To output the file as a CCTL
file, you should say

   :FILE LP;DEV=LP
   :FCOPY FROM=MYFILE;TO=*LP;CCTL

The  ";CCTL" parameter tells FCOPY to interpret the first character of
each record as a carriage control code.

Actually,  you  probably already know this,  since otherwise you would
have  asked  a  slightly  different  question. You've  done the :FCOPY
;CCTL,  and  you have still ended  up with pagination that's different
from what you had to begin with. Why?

Unfortunately,  not all the carriage  control information from a spool
file gets copied to a disc file with the >COPY command. In particular:

   * If your program sends carriage control codes to the printer using
     FCONTROL-mode-1s instead of FWRITEs, these carriage control codes
     will be lost with a >COPY.

   * If  the  spool  file you're copying is  a job $STDLIST file, the
     "FOPEN"  records  (which  usually  cause form-feeds  every time a
     program is run) will be lost.

   * And,  most  importantly,  if  your  program  using "PRE-SPACING"
     carriage control rather than the default "POST-SPACING" mode, the
     >COPYd spool file will not reflect this.

Statistically  speaking,  it  is  this third point  that usually bites
people.

The  MPE  default  is  that when you write  a record with a particular
carriage  control  code,  the  data will be output  first and then the
carriage   control  (form-feed,  line-feed,  no-skip,  etc.)  will  be
performed  --  this  is  called POST-SPACING.  However, some languages
(such  as  COBOL or FORTRAN) tell the  system to switch to PRE-SPACING
(i.e.  to  do  the  carriage  control operation  before outputting the
data),  and it is precisely  this command -- the switch-to-pre-spacing
command -- that is getting lost with a >COPY.

Thus,  output  that  was  intended  to come out  with pre-spacing will
instead  be  printed (after the >COPY)  with post-spacing; needless to
say,  the  output will end up looking  very different from what it was
supposed to look like.

What  can you do about this? Well,  I recommend that you take the disc
file  generated by the >COPY and add  one record at the very beginning
that  contains  only a single letter "A"  in its first character. This
"A"  is the carriage control code for switch-to-pre-spacing, and it is
the  very  code  that  (if I've diagnosed  your problem correctly) was
dropped  by  the  >COPY  command.  By re-inserting this  "A" code, you
should be able to return the output to its original, intended format.

Now,  this is only a guess -- I'm just suggesting that you try putting
in  this "A" line and seeing if the result is any better than what you
had  before. There might be other carriage control codes being lost by
the  >COPY; there might be, for instance, carriage control transmitted
using  FCONTROLs,  or  your  program might even  switch back and forth
between  pre-  and  post-spacing  mode  (which  is  fairly  unlikely).
However, the lost switch-to-pre-spacing is, I believe, the most common
cause of outputs mangled by the SPOOK >COPY command.

Robert  M.  Green  of Robelle Consulting Ltd.  once wrote an excellent
paper  called  "Secrets  of Carriage Control"  (published, among other
places,  in  The  HP3000  Bible,  available  from  PCI Press  at
512-250-5518); it explains a lot of little-known facts about carriage
control  files,  and  may  help  you  write programs  that don't cause
"interesting"  behavior  like  the  one  you've  experienced. Carriage
control  is  a tricky matter, and Bob  Green's paper discusses it very
well.


Q: I sometimes have to restore files from my system backups, which are
usually  about  seven reels. If the file I  want is, say, on the sixth
reel  (but  I  don't  know  it),  I have to mount  each one of reels 1
through  6  and  wait  for  :RESTORE  to read through  each one in its
entirety before it finds my file.

   I  know  we  ought to keep the full  SYSDUMP listing so that we can
tell  which  file  is  on  which  reel,  but that can  be a pretty big
printout (12,000 files = 200 pages). Isn't there some way for :SYSDUMP
to  put some sort of directory on  the first reel that maps every file
to its reel number?

A:  Well,  unfortunately  things aren't that  simple (are they ever?).
Let's look at how :SYSDUMP (or :STORE) works.

   Say  that  you  say :STORE @.@.@. At first,  :STORE has no idea how
many  reels this operation will consume (who knows -- maybe you have a
10,000' reel!).

   The  first thing :STORE writes to  tape is a "directory" containing
the  names  of  all  the  files being stored. Note  that so far :STORE
doesn't  know  which  reel each file goes  on, so this directory can't
contain this information.

   Then,  :STORE  begins to write the files  to tape. If you're lucky,
all  the files will fit on one reel; if not, at some point :STORE will
get an "end of tape" signal from the tape hardware, and will know that
a new reel needs to be started.

   Now,  it  would  be  great  if  :STORE  could  then go  back to the
directory  and  at  least mark each of the  files that couldn't fit on
this  reel. That way, when you  mount the reel, :RESTORE can instantly
figure out if the file is on it or not.

   Unfortunately, a tape drive is not a "read-write" medium. You can't
just  go  and, say, update the 10th record  of a file that's stored on
tape.  You can write a tape from scratch,  or you can read it, but you
can't take an already-written tape and modify it.

   Thus,  we now have a reel that  contains a directory with the names
of  ALL  the  files in the fileset but  only SOME of the actual files.
Only by reading all the way to the end of the reel can :RESTORE detect
that a particular file is actually on the next reel.

   We're done with reel #1 -- now to reel #2. At the beginning of reel
#2,  :STORE  also writes out the same directory  as it did on reel #1,
but also writes to the tape label THE NUMBER (relative to the start of
the fileset) OF THE FIRST FILE THAT IS ACTUALLY ON THIS TAPE. Then, it
writes  out  the files themselves, again hoping  that all of them will
fit  on  this reel. If they don't fit,  :STORE attempts to put them on
reel #3, reel #4, etc. In any case, the rule is that

   EVERY  REEL  CONTAINS THE DIRECTORY OF ALL  THE FILES IN THE ENTIRE
   FILESET,  PLUS THE NUMBER OF THE FIRST FILE ACTUALLY STORED ON THIS
   REEL.

This, again, is because:

   1.  There's  no  way for :STORE to know  in advance that any of the
      files won't fit on the reel;

   2.  Once  the reel is written, there's  no way for :STORE to update
      the directory on that reel short of re-writing the entire tape.

   Thus, the bad news is that :STORE doesn't keep any table that tells
which  reel each file is on. The good news is that it's still possible
to  restore  a file quickly even if you  don't know its reel number --
JUST GO THROUGH THE REELS BACKWARDS!

   That's  right  -- backwards. If you first  mount reel #1, then reel
#2,  then  reel  #3, etc., :RESTORE has to  read each entire reel from
beginning to end, since only by reaching the end of tape marker can it
tell  that  the file is not on this  reel. However, if you first mount
reel  #7, then reel #6, then reel #5, etc., :RESTORE will only have go
through  the DIRECTORY (a rather small chunk) of each reel to find the
reel that actually has the file.

   In  the example shown in the picture,  say that you try to :RESTORE
file  E.  First  you  mount reel 3, whose  directory indicates that it
contains  files F and G (since F is the 6th file on the reel). Without
having  to  read  through the entire reel,  :RESTORE will realize that
file  E  is on some previous reel; it  will then ask you to mount reel
#2, on which the file does indeed exist.

   Of  course, you might notice that  :STORE *could* have put the reel
numbers  INTO THE DIRECTORY OF THE  LAST REEL, so that simply mounting
the  last reel would automatically tell  you exactly which reel number
the  file  you  want  is on. (By the time  the last reel is generated,
STORE  naturally  knows  onto which reels all  of the other files were
written.)

   Unfortunately, the authors of MPE didn't choose to do this, much to
our  disadvantage. What most likely happened is that they didn't think
of  it  on  the  first  version of the system,  and when they realized
afterwards  that  this would have been a  good thing, it was too late.
:STORE/:RESTORE  faces  a  greater burden of  compatibility than other
aspects  of  MPE  --  not  only do tapes generated  by OLD versions of
:STORE  need  to  be  readable by NEW versions  of :RESTORE, but tapes
generated  by  NEW  versions  of  :STORE must also  be readable by OLD
versions of :RESTORE. Once HP put out one version of the system with a
directory format that had no room for the reel number, it was stuck.


   :FILE T;DEV=TAPE
   :STORE A,B,C,D,E,F,G;*T

   Reel #1:
     -------------------------------------
     | tape label: first file number = 1 |
     -------------------------------------
     | directory: A B C D E F G          |
     -------------------------------------
     | data of file A                    |
     -------------------------------------
     | data of file B                    |
     -------------------------------------
     | data of file C                    |
     -------------------------------------

   Reel #2:
     -------------------------------------
     | tape label: first file number = 4 |
     -------------------------------------
     | directory: A B C D E F G          |
     -------------------------------------
     | data of file D                    |
     -------------------------------------
     | data of file E                    |
     -------------------------------------

   Reel #3:
     -------------------------------------
     | tape label: first file number = 6 |
     -------------------------------------
     | directory: A B C D E F G          |
     -------------------------------------
     | data of file F                    |
     -------------------------------------
     | data of file G                    |
     -------------------------------------


Q:  I  know  that  many commands like  :EDITOR, :SYSDUMP, :COBOL, etc.
actually   run   program   files  in  PUB.SYS  called  EDITOR.PUB.SYS,
SYSDUMP.PUB.SYS,  COBOL.PUB.SYS, etc. What about :SEGMENTER? Obviously
there can't be a program SEGMENTER.PUB.SYS (file name to long) -- I've
heard  somebody  say  that  the  :SEGMENTER  program file  is actually
SEGDVR.PUB.SYS,  but  when  I  do a :LISTF on it  I see it has only 27
records! Could that be?

   Also, :SEGMENTER has a -PREP command in it that seems just like the
MPE  :PREP  command  -- does :SEGMENTER call  MPE to execute the -PREP
command?  I  tried  using the COMMAND intrinsic to  do a :PREP from my
program,  but  I  got  a  CI  error  12, "COMMAND  NOT PROGRAMATICALLY
EXECUTABLE".  Perhaps it's MPE's :PREP  command that calls SEGMENTER's
-PREP (I can't believe that HP would have written the same code in two
places); if so, can :ALLOCATEing SEGDVR.PUB.SYS make my :PREPs faster?

A:  Consider,  if you will: A humble  HP3000 user trying to fathom the
intricacies  of  the  MPE  SEGMENTER.  He believes that  typing an MPE
command  gets him into SEGDVR.PUB.SYS,  but, in reality, that innocent
"-" prompt is just the first signpost of

   THE TWILIGHT ZONE!

   OK, let's try to straighten all this out.

   First  of all, typing :SEGMENTER  is essentially equivalent to just
saying

   :RUN SEGDVR.PUB.SYS

(just like :EDITOR = :RUN EDITOR.PUB.SYS). You're right so far.

   Next,  as  you may have guessed, SEGDVR,  with its 27 records, just
doesn't have what it takes to execute all the thirty-odd commands that
the  SEGMENTER subsystem supports, from -PREP to -ADDSL to -COPY. Much
as  I dislike judging a program by how  big it is, 2500 or so words of
code aren't enough to do all that.

   All  that  SEGDVR actually does is PARSE  the commands typed at the
"-"  prompt.  Once  it determines what command  was typed and what the
parameters  were,  it passes it to  a system-internal procedure called
SEGMENTER  (not the command, but a  procedure in the system SL). MPE's
:PREP,  incidentally, also calls the SEGMENTER procedure, with exactly
the same parameters as :SEGMENTER's -PREP.

   So,  now things are simple. :SEGMENTER is just a parser -- the code
to actually do all the dirty work is in the system SL, right? Wrong.

   The  SEGMENTER procedure itself is only about 200-odd instructions.
All  that  it does is that it takes  its parameters -- things like the
command  number,  the  procedure/segment being  manipulated, the -PREP
capabilities,  maxdata, etc. -- and sticks them into a buffer which it
then  sends  (using  the  SENDMAIL  intrinsic)  to  a  program  called
SEGPROC.PUB.SYS.  The first command you execute from :SEGMENTER causes
this  process to be created as a son of SEGDVR.PUB.SYS (that's why the
first  command  you  type  in  :SEGMENTER  is  always slower  than the
subsequent   ones);   similarly,   when   you   do  a  :PREP  in  MPE,
SEGPROC.PUB.SYS is created as a son process of the CI. In either case,
the  SEGMENTER  procedure  is  used  to  communicate with  the SEGPROC
process using the SENDMAIL and RECEIVEMAIL intrinsics.

   Thus,  if  you  want  to  make sure that your  :PREPs go as fast as
possible,  you  should  :ALLOCATE  SEGPROC.PUB.SYS.  To speed  up your
:SEGMENTERs,   you   should   :ALLOCATE   both   SEGPROC.PUB.SYS   and
SEGDVR.PUB.SYS.

   Finally,  as  you  pointed  out,  although  you  can  easily  do  a
programmatic   :RUN   (using   the   CREATEPROCESS  intrinsic)  and  a
programmatic  compile (by CREATEPROCESSing the appropriate compiler in
PUB.SYS),  it's harder to do a programmatic :PREP. My suggestion would
be  to  put  the  PREP  command  into  a  file and  then CREATEPROCESS
SEGDVR.PUB.SYS with its $STDIN redirected to that file. Alternatively,
you  might  call  the  SEGMENTER  procedure  directly (or  even create
SEGPROC.PUB.SYS  as  a  son  process  and  communicate  with  it using
SENDMAIL   and  RECEIVEMAIL),  but  neither  of  these  approaches  is
documented by HP, and might change without notice.


   :SEGMENTER                            :PREP
       |                                   /
       v                                  /
   SEGDVR.PUB.SYS                        /
   (just a parser)                      /
       \                               /
        \                             /
         --------------\   /---------/
                        \ /
                         v
            SEGMENTER procedure (system SL)
                         |
                         |
               (via SENDMAIL/RECEIVEMAIL)
                         |
                         |
                         v
                   SEGPROC.PUB.SYS
                   (the workhorse)


Q: Today I came across a strange phenomenon upon which you may be able
to shed some light.

   I have two files on disc which contain identical data (I checked by
running FCOPY on them with the COMPARE option), so I would assume that
they would occupy the same number of sectors on disc. But I was WRONG!

   As you can see from the following :LISTF, the "vital statistics" of
the files are the same:

   FILENAME  ------------LOGICAL RECORD-----------  ----SPACE----
               SIZE  TYP        EOF      LIMIT R/B  SECTORS #X MX
   TN3WS       140B  FA        3000       6000  20     3311  8  8
   TN3WS86     140B  FA        3000       6000  20     1672  4  8

but the numbers of sectors are different!

   What's up?

A:  The  point that you raise is a  good one. Clearly the system needs
only 4 extents for your 3000 records; why is it that one of your files
has all 8 extents allocated?

   There are two possible reasons for this:

   * In the :BUILD command (and in the equivalent options of the FOPEN
     intrinsic),  you can specify both the MAXIMUM NUMBER OF EXTENTS A
     FILE  SHOULD  HAVE (in your case, it's  8 for both files) and HOW
     MANY  OF  THOSE EXTENTS ARE TO  BE ALLOCATED INITIALLY. You might
     say, for instance

       :BUILD X;DISC=100,10,3

     and have a file that looks like:

       FILENAME  ------------LOGICAL RECORD-----------  ----SPACE----
                   SIZE  TYP        EOF      LIMIT R/B  SECTORS #X MX
       X           128W  FB           0        100   1       33  3 10

     Although  all  the file HAS to have is  1 extent (to fit the file
     label), your :BUILD command requested that 3 extents be initially
     allocated. Similarly, saying

       :BUILD TN3WS;REC=-140,20,F,ASCII;DISC=6000,8,8

     will build the file

       FILENAME  ------------LOGICAL RECORD-----------  ----SPACE----
                   SIZE  TYP        EOF      LIMIT R/B  SECTORS #X MX
       TN3WS       140B  FA           0       6000  20     3311  8  8

     which has all 8 extents allocated.

     Why  would  you  want  to  initially  allocate more  extents than
     absolutely  necessary?  Well, if you only  allocate one extent to
     start  with, then as you're writing to the file, new extents will
     be  allocated as necessary. If you run  out of disc space when an
     FWRITE  operation  requires  a  new  extent to  be allocated, the
     FWRITE  will fail -- thus, it  would be possible for your program
     to  fail  halfway  through its execution,  having written 3000 of
     6000  records  but having no room on  disc to write the remaining
     3000. If you allocate all the extents initially, then any "OUT OF
     DISC  SPACE" condition will be reported when you :BUILD the file;
     once  the file is built, you're  guaranteed that all 6000 of your
     FWRITEs will succeed.

   * The other reason why a file would have more extents than its EOF
     seems to indicate is that although new extents are allocated when
     needed,   they   are  NOT  deallocated  when  they're  no  longer
     necessary.

     Say that you fill up your 6000-record file, causing all 8 extents
     to  be  allocated; then, you open it  with OUT access, wiping out
     all  the  file's records. The no-longer-needed  7 extents are not
     deallocated  by  this operation. Thus, your  file might have once
     been  full; then someone opened it with OUT access (throwing away
     all  the records in the file,  but not throwing away the extents)
     and  wrote  3000 new records to it.  The file's number of extents
     thus  reflects  the maximum number that  was ever needed for this
     file, even though that many may no longer be needed now.

Well,  those are the two most likely explanations for you. If you want
to  do something about the wasted space occupied by the empty extents,
you might rename the file, :FCOPY it, and then purge the old copy. The
new  file  will  be  built by :FCOPY to have  only one extent to start
with;  then,  new extents will be allocated  as needed, but never more
than  necessary.  Alternatively,  if  you use  VESOFT's MPEX/3000, you
could just use the %ALTFILE command:

   %ALTFILE TN3WS, EXTENTS=8

which  will  automatically  rebuild the file,  freeing all the extents
that are allocated but not used.


Q:  My program DBOPENs a database that I know may be redirected with a
:FILE  equation. How can I find out the TRUE filename of the database?
In other words, if my program opens database MYDB, and the user types

   :FILE MYDB=SUPERB.TEST.PROD

how  can my program figure out that it really is SUPERB.TEST.PROD that
it's accessing?

A:  There are several ways of finding  the fully qualified name of the
root file of the actual database being accessed by your program.

   One  solution  -- the non-privileged one  -- rests on the principle
that  if there's a :FILE equation for your database root file, you can
get  at  it using LISTEQ2 (on MPE IV),  LISTEQ5 (on MPE V), or the MPE
:LISTEQ  command (which, I believe, exists starting with about T-MIT).
You  can, for instance, use the COMMAND intrinsic to do a :LISTEQ into
a  disc  file,  and  then scan the disc file  for a :FILE command that
applies to your database.

   A  second solution, which requires privileged mode, is based on the
fact that a root file is an MPE file that can be FOPENed just like any
other MPE file. To open a database file, all you need to do is:

   * Be  in  privileged  mode when you FOPEN it  (AND when you do any
     other  file  system  operation,  such  as  an FGETINFO  or FCLOSE
     against it).

   * Specify  the  exact filecode of the file  to be opened (for root
     files, this is -400) in the FOPEN call.

Thus -- in SPL terminology -- you can say something like this:

   GETPRIVMODE;
   FNUM:=FOPEN (DBFILENAME, 1 << old >>,, ,,, ,,, ,,, -400);
   IF FNUM<>0 THEN
     BEGIN   << successful FOPEN >>
     FGETINFO (FNUM, REAL'FILENAME);
     FCLOSE (FNUM, 0, 0);
     END;
   GETUSERMODE;

As  you  see,  you  FOPEN  the root file  using whatever filename your
program  normally  uses  and then use FGETINFO  to return you the REAL
filename of the file (fully-qualified, of course).

   The  advantage  of  this  approach  is that it's  easier than doing
:LISTEQ  into  a  file  (or, on pre-T-MIT  systems, running LISTEQ2 or
LISTEQ5  with  output  redirected)  and  then  parsing  the  file. The
disadvantage is, of course, that it's privileged, and also that

   THE USER CAN ISSUE A :FILE EQUATION SUCH AS

     :FILE MYDB;DEL

   THAT WILL CAUSE YOUR PROGRAM TO PURGE THE ROOT FILE!

The FOPEN will open the file MYDB but then fall under the influence of
the  user's  file  equation, which will cause  the FCLOSE to purge the
file!  DBOPEN  won't  let  the user do that  (because it uses a system
internal  privileged  procedure  to  see exactly  what parameters were
specified  on the :FILE equation), but your ordinary unprotected FOPEN
won't.

   Finally,  there is a third  solution, also privileged. Whenever you
DBOPEN a database, the root file is FOPENed on your behalf by the file
system.  For any FOPENed file, you  can call FGETINFO against its file
number  and get its true filename (although for a privileged file, you
have  to be in privileged mode to  call FGETINFO). The trouble is that
to do this, you have to know the root file's file number, which DBOPEN
does  not  return  to you. DBOPEN does set  the first two bytes of the
database  name to be the "database id",  but that isn't related to the
file number.

   What  you  can do, though, is scan  ALL THE FILES THAT YOUR PROCESS
HAS  OPENED until you come across one whose filecode is -400 (the code
of  a  root file). Then -- provided  that your process has only opened
one  database -- that will be the file number of the opened database's
root file.

   Your program will then look something like this:

   FOR FNUM:=3 << min file# >> UNTIL 255 << max file# >> DO
     BEGIN
     GETPRIVMODE;   << FNUM may be privileged >>
     FGETINFO (FNUM, FILENAME,,,,,,, FILECODE);
     IF = << FNUM is a valid file number >> AND
        FILECODE=-400 << A root file >> THEN
       BEGIN
       GETUSERMODE;
       << we have the right filename >>
       ...
       END;
     GETUSERMODE;
     END;

As  you see, you go through all valid file numbers, from 3 (the lowest
file  number you can have) to 255 (the highest); if the file number is
invalid,  FGETINFO will set the condition code  to < -- if it's valid,
it  will set the condition code to =,  and then you can check the file
code to see if it's an IMAGE root file.

   All things considered, the first approach (LISTEQ into a disc file)
is  the most general (though quite  cumbersome) -- it doesn't endanger
your  database  at all, and it works  regardless of how many databases
you  have open. The third approach  (FGETINFOing all the file numbers)
works  only  if you have one database  open. I would recommend against
the  second approach (FOPENing the root  file yourself), since then --
by accident or by design -- the root file could be injured or deleted.


Q:  Help!  I have several files that I just can't get at.   I  try  to
copy   them,  :PURGE  them,  FOPEN  them,  and  what-not; I always get
prompted  for the lockword.  If I go into LISTDIR5  and  do  a  >LISTF
;PASS,   LISTDIR5 shows them as having lockwords of "/"; however, if I
specify  a "/" in response to the lockword  prompt,  MPE  gives  me  a
LOCKWORD VIOLATION.

   I  thought   that lockwords always had to be alphanumeric.  How did
my file get this "/" lockword?  How can I remove it?

A:  You've fallen victim to a somewhat unusual  MPE  file  system  bug
that  I've encountered a couple of times in the past.  If you call the
FRENAME   intrinsic and pass it a filename of, say, "NEWNAME/" instead
of  just "NEWNAME", the FRENAME intrinsic creates  the  new  file  not
with   an  empty lockword, but with a lockword of "/".  This will only
happen  if you use the FRENAME intrinsic -- MPE's :RENAME command will
not allow you to specify a filename with a "/" but no lockword.

   To  open the file, you'll have to  FOPEN  it  with  a  filename  of
"NEWNAME/"   --  again, a slash but no lockword.  As before, MPE won't
let  you specify such a  filename,  so  you  can't  just  say  ":PURGE
NEWNAME/"; however, you can say

   :FILE SALVAGE=NEWNAME/

and then say

   :PURGE *SALVAGE

or

   :RENAME *SALVAGE,NEWNAME

   So,  a  strange bug but a simple workaround.  Oddly enough, though,
there  IS a very good use for a filename with a slash but no lockword.
Say  you want to FOPEN a file but you  DON'T  want  to  prompt  for  a
lockword   (perhaps  your terminal is in block mode and a prompt would
only  mess things up).  If the file has no lockword, you'd much rather
have  the system immediately return an FSERR 92  (lockword  violation)
and  not output anything to the terminal.  To do this, just call FOPEN
with   a  filename of "FILE/.GROUP.ACCT".  If the file has no lockword
(or  a lockword of "/" caused by the bug mentioned above), the  system
will   open  it;  if  the  file  does have a lockword, the system will
return  an error but won't prompt the user for the lockword.  You  can
then,  for instance, prompt for the lockword yourself using V/3000, or
perhaps look up the lockword in some data file or something.

   Thus, to summarize:

                   File has     File has     File has bad ("/")
                   no lockword  lockword     lockword
                                "XXX"

FOPEN A.B.C        OK           Prompts      Prompts, but any answer
                                             is rejected

FOPEN A/XXX.B.C    Error 92     OK           Error 92

FOPEN A/.B.C       OK           Error 92     OK

   Opening  a file with a "/" but  no  lockword  is  useful  for  both
opening   files  with  "/" lockwords (caused by the MPE error) and for
avoiding lockword prompts for files that have normal lockwords.


Q:  What's   the  difference  between  $CONTROL   PRIVILEGED,   OPTION
PRIVILEGED,   and  OPTION  UNCALLABLE?  I always thought that $CONTROL
PRIVILEGED  would make all the procedures in my program run in PM, but
it seems that it doesn't.  What's going on?

A:  First of all: what are OPTION PRIVILEGED  and  OPTION  UNCALLABLE?
These   are  both  SPL keywords that are placed in a procedure header,
and  both of them have to do with PM capability; however, there  is  a
world   of  difference  between  them.   Consider  the  following  SPL
procedure:

   PROCEDURE P;
   OPTION UNCALLABLE;
   BEGIN
   ...
   END;

This  simply means that ANYBODY WHO CALLS P MUST CALL IT  FROM  WITHIN
PRIVILEGED MODE.  P is "UNCALLABLE" from normal user mode.

   This  can   apply  to  procedures inside a program file but is more
often  used in SL procedures.  For instance, the system SL contains  a
Now  the very point of system SL procedures is that they can be called
from  user programs -- DBOPEN, FREAD, PRINT, etc. are  all  system  SL
procedures;   however,  we  certainly don't want user programs to call
the  SUDDENDEATH SL  procedure.   Since  SUDDENDEATH  is  declared  as
OPTION   UNCALLABLE,  only  code that is running in privileged mode --
whether  in the system SL or in a program file -- can  call  it.   Any
other   calls  would cause the calling program to abort with a PROGRAM
ERROR #17: STT UNCALLABLE.

   OPTION  PRIVILEGED, however, does almost the opposite.  Instead  of
indicating   that  the CALLER of this procedure must be privileged, it
indicates   that  the  procedure  itself  is  to  always  execute   in
privileged    mode,  REGARDLESS  OF  WHETHER  OR  NOT  ITS  CALLER  IS
PRIVILEGED.  Thus, saying:

   PROCEDURE P;
   OPTION PRIVILEGED;
   BEGIN
   ...
   END;

means  that regardless of whether P's caller is in privileged mode  or
not,   P's  code  will always execute in PM.  What's more, MPE doesn't
really   keep  the  PRIVILEGED/  non-PRIVILEGED   information   on   a
procedure-by-procedure   basis (although the UNCALLABLE/non-UNCALLABLE
information  IS kept for each procedure).  An entire SEGMENT is either
permanently  privileged (all its code executes in privileged mode)  or
non-privileged  (all its code executes in whatever mode its caller was
in).   An OPTION PRIVILEGED procedure "taints" the entire segment it's
in,   causing  all  procedures  in  the  segment  to always execute in
privileged  mode (in which no  bounds  checking  is  done  and  system
failures are easy to cause).

   At  this point, we might also mention that what we're talking about
here  is PERMANENTLY PRIVILEGED mode.  Code that calls GETPRIVMODE and
GETUSERMODE  WILL ONLY BE IN PRIVILEGED MODE BETWEEN THE "GETPRIVMODE"
AND   THE  "GETUSERMODE"  CALLS.  However, if we use OPTION PRIVILEGED
(or  $CONTROL PRIVILEGED) instead of GETPRIVMODE/GETUSERMODE calls (as
is  usually done for SL procedures),  then  entire  segments  will  be
marked as permanently privileged.

   $CONTROL  PRIVILEGED   is exactly like an OPTION PRIVILEGED for the
program's  "outer block".  Say that you have a  program  that  has  no
procedures   in  it  --  just the main body.  $CONTROL PRIVILEGED will
indicate  that the main body itself will  be  permanently  privileged,
just   as a procedure's OPTION PRIVILEGED indicates that the procedure
will   be  permanently  privileged.   If  your  program  has   several
procedures   in  several segments, a $CONTROL PRIVILEGED will indicate
that   THE  SEGMENT  THAT  CONTAINS  THE  PROGRAM'S  OUTER  BLOCK   is
permanently   privileged;  other segments need not be unless they have
OPTION PRIVILEGED procedures in them.

   A  $CONTROL PRIVILEGED thus does NOT apply  to  the  entire  source
file;   it  only  applies to the outer block and, by extension, to the
segment that contains the outer block.

   So, the important distinctions to remember are:

   * OPTION PRIVILEGED (procedure executes in priv mode)  vs.  OPTION
     UNCALLABLE (procedure can only be called from priv mode);

   * permanently   privileged  (where  the  entire  segment is always
     privileged)  vs. temporarily privileged  (where  you're  only  in
     priv   mode  between  the  GETPRIVMODE  call  and the GETUSERMODE
     call);

   * and  $CONTROL  PRIVILEGED  (which  indicates  that  the  segment
     containing   the outer block is privileged) vs. OPTION PRIVILEGED
     (which  indicates that the segment containing this  procedure  is
     privileged).

   Finally,  one  important note:  the Intrinsics Manual describes two
intrinsics   (GETPRIVMODE  and  SWITCHDB)   as   "O-P",   or   "option
privileged"   (see  p.  2-1  of  the FEB 1986 edition).  This does NOT
really  mean "option privileged"; most  intrinsics  (including  FOPEN,
QUIT,   etc.)  are  declared  as  OPTION  PRIVILEGED because they must
execute  in privileged mode.  Rather, the Intrinsics Manual's  "option
privileged" means two things:

   -  for SWITCHDB, it really means "OPTION UNCALLABLE"; if you try to
     call   SWITCHDB  from  user mode, your program will abort with an
     STT UNCALLABLE error;

   -  for GETPRIVMODE, it really means neither OPTION  UNCALLABLE  nor
     OPTION   PRIVILEGED;  rather,  it  means that the calling program
     must  have been :PREP'ED WITH CAP=PM, regardless  of  whether  or
     not   it  is  actually in privileged mode at the time of the call
     (which is the distinction that OPTION UNCALLABLE uses);

Keep  this in mind and be aware that neither of these definitions  are
the true meaning of OPTION PRIVILEGED.


Q:  What  is the "COLD LOAD ID"  number that LISTDIR5 outputs with its
LISTF  command?  It  seems  to be the same  as the "cold load identity
field" in the :LISTF ,-1 output, but I can't for the life of me figure
out what it means.

A:  A  file's file label contains a lot  of data about the file -- the
file's  name, its filecode, the locations of its extents on disc, etc.
As  a  general  rule, pretty much everything  that FOPEN might need to
know about a disc file before opening it is kept in its file label.

   Say  that  you  FOPEN  a file for  exclusive access. All subsequent
attempts  to  FOPEN  the  file should fail, since  you have it open it
exclusively  --  but  how  is  the  system  going to  know this? Well,
whenever  a file is accessed in any way (by FOPEN, by :RUN, by :STORE,
or  by  :RESTORE),  the  appropriate  bit  in  the  file label  is set
indicating the type of access. Subsequent access attempts will look at
those  file  label  bits  to  see if the file  is being accessed in an
incompatible  way. This is what  prevents concurrent exclusive FOPENs,
:PURGEs of running programs, :STOREs of files that are being modified,
and  so on. There are in fact several such fields in the file label --
fields  that  are  used  to indicate the way in  which a file is being
accessed:

   * The  store bit, restore bit, load  bit, exclusive bit, read bit,
     and write bit (word 28);

   * And, the so-called FCB vector (words 32-33) that indicates where
     an FOPENed file's control block is located in memory -- this way,
     two  people  who  FOPEN  the same file can  easily share its file
     control block.

   Now,  so  far,  all  this  has nothing to do  with the coldload id.
However,  ask yourself this: What if the system goes down while a file
is being accessed?

   The file's access bits and FCB vector are still set to indicate the
file  is in use; when you reboot  the system, the file will apparently
be  in  the  process of being accessed.  Further attempts to FOPEN the
file  exclusively  will fail, shared FOPENs will  try to share the FCB
pointed to by the (now entirely invalid) FCB vector, and so on.

   Once  upon  a time there was a bug  like this in IMAGE -- the IMAGE
root file had its Data Base Control Block's data segment number stored
in  one  of its records (to make it  easy for subsequent openers of an
IMAGE  databases  to  find  the  DBCB  and share it);  when the system
crashed  and was then rebooted, IMAGE  still thought that the DBCB was
stored  in  that  data  segment,  which now didn't  exist or contained
entirely different data. Naturally, this caused a good deal of grief.

   Therefore,  there  must  be  some  way for the  system to determine
whether  the  "transitory" data in the file  label -- data that is set
when  a  file  is  open  and is supposed to be  reset when the file is
closed  --  is  really  correct  or just a  carry-over from a previous
"incarnation"  of the system. Since by the very definition of a system
failure  the  system  can't close the files  normally (as opposed to a
normal  =SHUTDOWN, which will normally close the files), there must be
some other solution.

   One  idea  that  comes  to  mind is to, at  system startup time, go
through all the file labels in the system and reset their "transitory"
data.  Unfortunately,  if  a  system has 30,000  files and can perform
about  30  disc  I/O's  per  second,  this would take  1000 seconds or
somewhat  over 15 minutes. Not a  totally unreasonable amount of time,
but  not very quick, either. When a system has just crashed and you're
rebooting  it,  what  you  care  about  most is getting  it back up as
quickly  as possible, and not taking  time going through the labels of
files which, for the most part, may have not been accessed in months.

   This  is  where  the cold load id comes  in. It's not really a cold
load  id,  but  rather  a "restart id"; whenever  a system is rebooted
(with  a  warmstart,  coolstart,  cold load, update,  or reload), this
number  is  incremented;  whenever a file is  opened, the current cold
load id is put into the file's file label.

   This  way,  whenever  you FOPEN a file, the  file system has a very
simple way to see if the transitory data in the file label is valid --
if the current system cold load id is equal to the cold load id in the
file label, then the data is valid; if it is different, this means the
file  has not been accessed at all since the last system re-start, and
even if the file label indicates that the file is in use, this is only
a  carry-over  from a previous system  incarnation which crashed while
the file was open.

   Thus, what you need to know about a cold load id is:

   1)  You should never really have to care about it, since everything
      the system does with it is behind your back;

   2)  It is used for making sure that, when the system crashes with a
      file  open, subsequent incarnations of the system will not think
      the file still open because of the access bits that may still be
      set in the file label;

   3) It should properly be called not a "cold load id" but a "restart
      id",  since it's reset at every system re-boot, from a warmstart
      on up.

   Actually,  there are circumstances in which  you may be able to use
cold  load id's yourself. Say that you  have a program that keeps some
data  in a file while the file is  opened -- for instance, you want to
make  it easy for people to see who's doing what to the file, so every
time  your  program opens the file, it  writes the opening user's name
into some special place, and every time it closes the file, it deletes
the user's name.

   What  happens  when the system crashes  and is then re-booted? Why,
the  file  contains  the names of all those  people who were using the
file when the system crashed but are obviously no longer using it now.
You need some way of figuring out whether this data was written to the
file before or after the last system failure.

   What  you  can  do is, every time you  put some of this "transitory
data"  (in this case, the file user's name) into the file, also put in
the  current  cold load id. Then, when you  look at this data, you can
check  the current cold load id against  the one stored in the file --
if  it doesn't match, this means that  the transitory data in the file
was written before the last system re-start and is thus obsolete.

   The  only  other  thing you need to know  is how to get the current
system  cold  load  id.  There's  no  intrinsic  that  gets  you  this
information,  but there is  an (undocumented) non-privileged procedure
that can get it for you:

   INTEGER PROCEDURE SYSGLOB (WORDNUM);
   VALUE WORDNUM;
   INTEGER WORDNUM;
   OPTION EXTERNAL;

This  procedure  lets  you  get  an arbitrary value  from the system's
"SYSGLOB"  area,  and it so happens that  word %75 (decimal 61) is the
cold load id. Thus, just saying (in SPL)

   INTEGER PROCEDURE SYSGLOB (W); VALUE W; INTEGER W; OPTION EXTERNAL;
   ...
   COLDLOADID:=SYSGLOB(%75);

or (in FORTRAN)

   INTEGER SYSGLOB
   ...
   ICOLDLOADID = SYSGLOB (\%75\)

or (in COBOLII)

   CALL "SYSGLOB" USING \%75\ GIVING COLD-LOAD-ID.

will get you the cold-load id.

The  following  is  a re-run of a Q&A  that appeared in the March 1987
issue  of Interact. I'd like to print  it again because it describes a
rather  mysterious-seeming situation that is all too easy to get into;
in  fact, recently I've had some people  call me and ask me about this
very problem.


Q:  On several occasions I've had  very difficult problems  caused  by
"garbage"   characters  (escapes,  nulls,  control  characters, etc.).
Sometimes  they're in my source files; sometimes they're  in  my  data
files;  in any case, they're very hard to see at first glance and even
DISPLAY FUNCTIONS doesn't show some of them (e.g. nulls).

   I'd  like   to have a job stream that goes through a few of my most
important  files and checks to see if there are any garbage characters
inside  them.  How can I  --  preferably  without  writing  a  special
program -- have a job stream check a file for garbage characters?

A:  This   is  a  classic  "MPE  PROGRAMMING"  question -- how to do a
seemingly  complicated task without having to write a special  program
to  do it.  As always, MPE programming problems require a good deal of
ingenuity   and,  to  be  frank,  a  rather  twisted way of looking at
a problem.

   As  I'm sure you're aware, :FCOPY has an option called  ;CHAR  that
outputs   the  contents  of  the  ;FROM=  file  REPLACING  ALL GARBAGE
CHARACTERS  BY DOTS.  For instance, if your ;FROM= file line  is  "AB"
followed by a null (ascii 0) followed by "CD", then an

   :FCOPY FROM=MYFILE;TO;CHAR

will output the line as

    RECORD 0 (%0, #0)

   00000: AB.CD

The null character was replaced by a ".".

   Now,  FCOPY may have this option, but of what good is it to us?  If
FCOPY   had,  for  instance, set some JCW to 1 if at least one garbage
character  was replaced by a "." and to 0 if none were, then  we'd  be
home   free  --  all we'd have to do is do the :FCOPY ;CHAR, check the
JCW, and that's that.

   Unfortunately,  FCOPY has nothing like  this;  it  just  takes  the
;FROM=    file  and  copies  it  (with  whatever  transformations  are
appropriate) to the ;TO= file.

   Here  is where we have to be tricky.  First  of  all,  we  have  to
remember   that  FCOPY  has another (less well-known) parameter called
;NORECNUM.   This merely says that (when you specify ;CHAR, ;OCTAL, or
;HEX)  the output should  NOT  contain  the  "RECORD  0"  or  "00000:"
headers.  Thus, an

   :FCOPY FROM=MYFILE;TO;CHAR;NORECNUM

will output the line

   AB.CD

without any record numbering information.

   So, this is what we do.  First we say:

   :BUILD MYFILENG;REC=<<must be the same as MYFILE>>
   :FCOPY FROM=MYFILE;TO=MYFILENG;CHAR;NORECNUM

"MYFILENG"  is  now EXACTLY the same as MYFILE except that all garbage
characters have been replaced with "."s.  Now, we say

   :FCOPY FROM=MYFILE;TO=MYFILENG;COMPARE

This  will now compare MYFILE and MYFILENG to see  if  there  are  ANY
DIFFERENCES  between the two files.  Obviously, these differences will
ONLY  exist if there were some garbage characters in MYFILE (since all
non-garbage   characters  are  copied  exactly).   Fortunately, if the
:FCOPY  ;COMPARE finds at least one difference,  it  sets  JCW  to  be
equal to FATAL (in batch).  Therefore, we can say:

   :JOB FINDGARB,ROGER.DEV;OUTCLASS=,1
   ...
   :BUILD MYFILENG;REC=<<must be the same as MYFILE>>
   :FCOPY FROM=MYFILE;TO=MYFILENG;CHAR;NORECNUM
   :SETJCW JCW=0
   :FCOPY FROM=MYFILE;TO=MYFILENG;COMPARE
   :IF JCW<>0 THEN
   :  TELL ROGER.DEV !!! MYFILE has garbage characters !!!
   :ENDIF
   :PURGE MYFILENG
   :EOJ

   In  fact, by looking at the job stream $STDLIST, you can even  find
out   in  what  record  and  at  what  word  in  the  record the first
discrepancy  (i.e. the first  garbage  character)  occurred  --  FCOPY
prints   this  information for you.  Note, however, that you shouldn't
try  to find all the garbage characters  by  doing,  say,  an  ":FCOPY
;COMPARE=100".    If  you  specify  a maximum number of compare errors
(100  in this case), FCOPY won't actually set JCW unless at least that
many errors were found.

   Finally,  note that the file you're examining in  this  way  should
contain   only  ASCII data -- if it's a data file that contains binary
data  (e.g. word 5 is viewed as an integer), that data may  appear  as
"garbage"   to FCOPY, since it views the entire input record as ASCII.
Also  note that "garbage" means to FCOPY any character with  an  ASCII
value   of  0  to  31  or  127  to  255 -- this includes all the ASCII
characters  with the parity bit set,  and  may  also  include  various
"native language character set" characters.


Q:  With the advent of Spectrum we are using PASCAL to  write  systems
level code that formerly would have been written in SPL.

In  SPL   there was a close correlation between program statements and
machine  language instructions, but in PASCAL, it is  often  difficult
to   tell  how small and/or efficient the resulting code will be.  The
PASCAL/3000 manual is of limited help regarding these concerns.

For example, there are four ways of appending string B to string A:

   (1)   A := A + B;
   (2)   STRAPPEND (A, B);
   (3)   STRMOVE (STRLEN(B), B, 1, A, STRLEN(A) + 1);
   (4)   STRWRITE (A, STRLEN(A) + 1, T, B);

All  four of these statements should do the same thing (assuming  that
there's   no  error,  i.e.  STRLEN(B) > 0 and STRLEN(A) + STRLEN(B) <=
STRMAX(A)).   Which of them will  generate  the  "best"  object  code;
which  will generate the "worst"?  Will the answer to this question be
any different on MPE/XL?

A:  You raise a very interesting question.  Indeed, in many high-level
languages   a  single  simple-looking operation can actually do a vast
amount  of work and take a long time to do it.  In fact,  you  needn't
look  as far as a third-generation programming language -- do you know
how   much stuff a simple PCAL (a one-word instruction) has to do, and
how  long it might take?  If it needs to swap in the code segment from
disc,  this innocent-looking instruction  might  easily  take  you  30
milliseconds or more!


   There  is   one  sure  way  to  find  out  which  of the above four
operations  is the fastest -- try them and see.  The program I  hacked
up looks like this:

   $STANDARD_LEVEL 'HP3000'$
   PROGRAM TIMINGS (INPUT, OUTPUT);

   VAR I, T, TIME: INTEGER;
       A, B: STRING[256];

   FUNCTION PROCTIME: INTEGER;  INTRINSIC;

   BEGIN
   TIME:=PROCTIME;
   FOR I:=1 TO 10000 DO
     BEGIN
     SETSTRLEN (A, 30);
     SETSTRLEN (B, 30);
     END;
   TIME:=PROCTIME-TIME;
   WRITELN ('CONTROL':20, TIME);
   TIME:=PROCTIME;
   FOR I:=1 TO 10000 DO
     BEGIN
     SETSTRLEN (A, 30);
     SETSTRLEN (B, 30);
     A:=A+B;
     END;
   TIME:=PROCTIME-TIME;
   WRITELN ('A+B':20, TIME);
   TIME:=PROCTIME;
   FOR I:=1 TO 10000 DO
     BEGIN
     SETSTRLEN (A, 30);
     SETSTRLEN (B, 30);
     STRAPPEND (A, B);
     END;
   TIME:=PROCTIME-TIME;
   WRITELN ('STRAPPEND':20, TIME);
   TIME:=PROCTIME;
   FOR I:=1 TO 10000 DO
     BEGIN
     SETSTRLEN (A, 30);
     SETSTRLEN (B, 30);
     STRMOVE (STRLEN(B), B, 1, A, STRLEN(A)+1);
     END;
   TIME:=PROCTIME-TIME;
   WRITELN ('STRMOVE':20, TIME);
   TIME:=PROCTIME;
   FOR I:=1 TO 10000 DO
     BEGIN
     SETSTRLEN (A, 30);
     SETSTRLEN (B, 30);
     STRWRITE (A, STRLEN(A)+1, T, B);
     END;
   TIME:=PROCTIME-TIME;
   WRITELN ('STRWRITE':20, TIME);
   END.

As  you see, we use the PROCTIME intrinsic to determine the number  of
CPU   seconds  consumed  by  the program so far; a PROCTIME before and
after  each operation will tell us  exactly  how  much  CPU  time  the
operation took.

   Note  also   that I had to make an arbitrary assumption -- I assume
that  both strings are 30 characters long.  Quite possibly one of  the
methods   might  have  a longer "start-up" time but be faster for each
character;  in that case,  copying  longer  strings  might  make  that
method   more  efficient,  while copying shorter strings might make it
less efficient.

   Finally,  note that none of the loops includes JUST  the  operation
we   need  to  test.   For one, we have to reset both strings to their
initial  values; for another, the FOR loop itself takes a  non-trivial
amount   of  time  (to  increment  the variable, compare, and branch).
This  is why the first loop is a "control" loop intended to figure out
how long everything EXCEPT the concatenation actually takes.

   So,  what's the result?  Well, before I ran the program, I  decided
to   do  a  little  self-test -- I guessed what I thought the relative
rankings would be.  You might want to try to do this, too.

   Intuitively,  my  general  idea  is  "the  more  general-purpose  a
function,   the  longer  it'll take".  This is because a function that
does  only one  thing  can  make  all  sorts  of  efficiency-improving
assumptions that the general function can't make.

   It  seems   that  the function most uniquely adapted to this job is
STRAPPEND.   Whatever the fastest way of  appending  two  strings  is,
there's   no  earthly  reason why STRAPPEND couldn't use it.  Thus, it
stood to reason that the STRAPPEND (method (2)) would be the best.

   My  guess for second place was the A := A + B; third place went  to
the  STRMOVE.  Don't ask me why I didn't guess the other way around --
perhaps STRMOVE's five parameters scared me.

   I  was   absolutely  sure that the STRWRITE would be the slowest of
them  all.  This is probably because I've  been  biased  by  FORTRAN's
formatter   (the  dog  to  end  all dogs).  General-purpose formatting
facilities  are very useful (although PASCAL's  is  quite  ugly),  but
they're almost never very fast.

   So  that was my "educated" guess.  What  were  the  results  on  my
MICRO/XE?

   CONTROL         257
   A+B            1791
   STRAPPEND      1073
   STRMOVE        2042
   STRWRITE      15733

The  numbers are in milliseconds, and indicate the time  taken  to  do
each   10,000-iteration  loop.   Thus,  each  STRWRITE  took about 1.6
milliseconds.

   Curiously  enough, my intuition was rather confirmed by  the  test.
(I   guessed  before I saw the numbers -- scout's honor!) STRWRITE was
even  slower than I though; the other  three  were  pretty  even,  but
STRAPPEND (the least general of them) was the champ.

   At  this point, I asked a Spectrum-possessing friend of mine to run
the   same test on his machine.  Since the one thing he's forbidden to
do  on his machine is test Spectrum performance, I will protect him by
leaving him anonymous.  Let's just call him "Deep Code".

   The  results with the Spectrum Native Mode PASCAL/XL compiler  were
somewhat different:

   CONTROL           1
   A+B              11.8
   STRAPPEND        20.1
   STRMOVE           6.5
   STRWRITE         58.8

(To  prevent comparisons between Spectrum and MPE/V, all the times are
given  relative to the control time -- no absolute timings  should  be
inferred.)

   As  you   see,  the  cumbersome-seeming STRMOVE was actually faster
than either the A+B or STRAPPEND.  STRWRITE was still way behind.

   Finally,  I decided to compile the program with range checking  off
($RANGE OFF$) on MPE/V.  The results were now:

   CONTROL         256          (with $RANGE ON%: 257)
   A+B            1737          (with $RANGE ON$: 1791)
   STRAPPEND      1070          (with $RANGE ON$: 1073)
   STRMOVE         875          (with $RANGE ON$: 2042)
   STRWRITE      15697          (with $RANGE ON$: 15733)

As  you can see, the results are mostly unchanged EXCEPT for  STRMOVE,
which   is  now  the  fastest  of  the lot, more than twice as fast as
before!

   So  there's an answer -- STRAPPEND is fastest on MPE/V with  $RANGE
ON$,   STRMOVE  is  fastest with $RANGE OFF$ or on MPE/XL.  This is an
answer, but I don't think that it's THE answer.

   In  this  day  and  age,  the  most  valuable  commodity  in  a  DP
department   is not computer time -- it's programmer time.  If all you
care  about is efficiency, you might as well stick with assembly code.
Consider  also that the efficiency of most of  your  code  is  largely
irrelevant;  the overwhelming majority (90% or more) of your program's
time is spent in less than 10% of its code.

   My general philosophy, then, is this:

   * DESIGN FOR EFFICIENCY.

   * CODE FOR SIMPLICITY.

   * OPTIMIZE LATER.

   When  you're   making  fundamental  decisions  about your algorithm
(sequential  access vs. direct access,  B-trees  vs.  hashing,  etc.),
efficiency   should  play a major role in your thinking -- once you've
committed  to one fundamental approach that  later  proves  too  slow,
it's often too difficult to change to another approach.

   However,  when   I  write  my  code, I almost invariably choose the
simplest,   most  straightforward   approach   --   for   reliability,
readability,   and maintainability.  It so happens that it's about 50%
faster  to shift right by 1 bit than to divide by 2.   However,  if  I
want to average two numbers, I'd much rather say:

   AVERAGE:=(MAXIMUM+MINIMUM)/2;

than

   AVERAGE:=(MAXIMUM+MINIMUM)&ASR(1);

Looking  at the first statement, I instantly see what's going on -- it
documents   itself.  To understand the second statement, I really have
to  think about  it.   Similarly,  even  if  it  were  faster  to  say
something like

   AVERAGE:=DIVIDE(ADD(MAXIMUM,MINIMUM),2);

(where   DIVIDE   and  ADD  are  presumably  super-efficient  built-in
functions), I'd still rather say

   AVERAGE:=(MAXIMUM+MINIMUM)/2;

The (MAXIMUM+MINIMUM)/2 is just a lot easier to read and understand.

   What's  more, chances are that any little optimization I  might  do
to   this  statement  would be of virtually no consequence.  As I said
before,  the overwhelming majority of a program's time is spent  in  a
very   few  places.   You can spend weeks writing all your code in the
most  "efficient" manner (and months fixing the bugs caused by all the
extra  complexity), when  you  could  have  just  spent  a  few  hours
optimizing those few statements that take the most time.

   What's  more, I've found that I ALMOST NEVER know what parts of the
program  will actually be the most frequently used ones.  I just write
everything   in the simplest, cleanest way and then use HP's wonderful
APS/3000  program to find out where the "hot spots" are.  APS/3000 can
tell  you EXACTLY where your program is spending most of its  time  --
armed  with this data, you can often change a half dozen lines and get
a two-fold performance improvement.

   Therefore,  my recommendation is to use whatever mechanism looks to
you   to  be  the easiest and most understandable.  I'd recommend that
you say

   A := A + B;

because  that seems to me to be  the  clearest  solution.   Similarly,
even   though  STRWRITE  is so horribly slow, I'd still recommend that
you  use it in cases where it best represents what you're  doing  (for
instance,  if you want to use the ":fieldlength" syntax or you want to
concatenate numbers as well as strings).

   Then,  after you've written the program, run APS/3000.  If you find
that   this  statement  is  in a particular tight loop and is taking a
large  portion of the program's run time, then you can optimize it  to
your   heart's  content.  However, don't spend too much effort chasing
an  instruction here and there.  Your time is a lot more valuable than
the  computer's. First, a few words in response to some of the Letters
to the Editor in the February 1988 issue.

   Mr.  Gerstenhaber of Computation &  Measurement Systems in Tel-Aviv
commented on avoiding EOF's on PASCAL READLNs in case the input starts
with  a  ":".  He quite correctly points out  that if you issue a file
equation  :FILE  INPUT=$STDINX (the "X" somehow  dropped off the :FILE
equation  in  the  printed copy of his  letter), PASCAL will read from
$STDINX and will not get an error on ":" input.

   My  original  answer  recommended that you put  in a "RESET (INPUT,
'$STDINX  ');" statement into your PASCAL program -- this solution has
the  advantage  of  making  the program completely  stand-alone, so it
doesn't  require a :FILE equation  to run properly. Mr. Gerstenhaber's
solution,  on  the other hand, has the  advantage of not requiring any
source  code  changes  (although  the  program  won't run  quite right
without   a  :FILE  equation).  Both  solutions  are  very  reasonable
alternatives.

   Mr.  van Herk of Mentor Graphics  in the Netherlands inquired about
saving  scheduled/waiting jobs across a  coldload and about "letting a
session log itself off after a certain idle period". These issues were
apparently the subjects of previous Q&A questions (which were answered
by Mr. N. A. Hills).

   The  first question was originally asked in  the June 1987 Q&A -- a
user did a coldload once a week (as recommended by HP), and found that
his  scheduled  jobs  were  deleted. He couldn't  just re-submit them,
since  they  were originally submitted by  users -- the system manager
doesn't  know  the original job stream filenames,  and in any case the
original files might have already been modified or purged.

   As Mr. van Herk quite correctly points out, the contributed library
JSPOOK  program addresses this very problem.  I'm not sure exactly how
much  it  preserves  --  whether  it  works for WAITing  jobs only, or
SCHEDuled  ones  as  well;  I  suspect that there  are newer and older
versions  of  JSPOOK  in  various  places  that do  slightly different
things.

   However,  JSPOOK is definitely the place to start -- look for it on
the contributed library; it might very well do exactly what you want.

   "Letting a session log itself off after a certain idle period" is a
different  story.  I  guess  that  what  the user wanted  was to abort
sessions  whose user has walked away from his terminal (thus causing a
potential security threat) or is just signed on and not doing anything
(which may, for instance, use precious ports on your port selector).


Q:  I've  always  wondered how IMAGE calculates  the maximum number of
extents  for a dataset. Most of my  datasets end up having 32 extents,
but  some smaller datasets have fewer. Why is this? Is it some sort of
MPE limitation, or is it IMAGE's own choice.

A:  My old IMAGE Manual (March 1983 version) has little to say on this
topic:  "Each  data  file  is  physically  constructed from  one to 32
extents  ..., as needed to meet the capacity requirements of the file,
subject to the constraints of the MPE file system" (p. 2-10).

   To  get the answer, I had to decompile the IMAGE code in the system
SL.  As best I could tell from the machine instructions, the algorithm
was:

   #extents = flimit / 64 + 1

In  other words, if the file limit ends up being 920 records (the file
limit  having been calculated from the  capacity, the record size, and
the blocking factor), the number of extents will be

   920/64 + 1 = 14 + 1 = 15 extents

(note that the division rounds down).

   The  principle here seems to be  to avoid really small extents. The
best  reason  for  this  is  disc caching (although  I think the above
algorithm  might have been chosen before disc caching was implemented)
--  the  most  that  MPE can cache is one  extent; if extents are very
small, you'll lose much of the benefit of caching.

   Usually,  these numbers of extents should  work quite well for you.
If  for  some  reason you want to change  them, though, you can't just
issue a :FILE equation at DBUTIL >>CREATE time (since >>CREATE ignores
file equations). One thing you might do is use MPEX's

   %ALTFILE datasetfilename;EXTENTS=newnumextents

(%ALTFILE  ;EXTENTS= lets you change the maximum number of extents for
any disc file). However, as I said, you'd rarely want to do this.


Q:  I'm trying to call a PASCAL procedure from a FORTRAN program and I
get  a loader error that says "INCOMPATIBLE FUNCTION FOR" and then the
name  of  the  PASCAL  procedure.  I  know  that  PASCAL  is  a strict
type-checking  language,  but  my  FORTRAN  calling sequence  seems to
exactly  match  the  PASCAL  procedure's  formal parameter  list! (The
PASCAL  procedure returns an integer  and takes several simple integer
by-reference  parameters, which is exactly what  I pass to it.) What's
going on here?

A: The compilers, the segmenter, and the loader support a little-known
mechanism  known as the CHECK LEVELS.  This allows MPE (when :PREPping
together  separately compiled code or when calling an SL procedure) to
make  sure  that the caller and the callee  both have the same idea of
how the procedure parameters should be passed.

   The  procedure that is called (the  "callee") may have (in its USL,
RL,  or SL) information describing  its parameter types; the procedure
that  calls  it  (the  "caller")  may have  information describing the
parameter  types  that  it  expects. If the two  don't match, an error
message is printed.

   By default, SPL generates all its procedures (and all its procedure
calls)  with OPTION CHECK 0, which indicates that NO CHECKING IS TO BE
DONE.  Thus, if you try to call an average system SL procedure (almost
certainly  written  in SPL), no parameter checking  will be done -- if
you  mis-specify it in an OPTION  EXTERNAL declaration, you're in deep
trouble.

   Now,  FORTRAN  and PASCAL by default  generate all their procedures
(and  all  procedure calls) with OPTION  CHECK 3, which indicates that
the NUMBER OF PARAMETERS, THE RESULT TYPE, and EACH PARAMETER TYPE are
to  be checked. If FORTRAN generates an OPTION CHECK 3 procedure call,
but  the called procedure is OPTION CHECK 0, no checking will be done;
if  SPL  generates  an  OPTION CHECK 0 procedure  call, but the called
procedure  is  OPTION  CHECK 3, no checking  will be done. However, if
both  the procedure call and the  procedure itself are OPTION CHECK 3,
full checking will be done to make sure that the caller's and callee's
procedure declarations are identical.

   So, what's the big deal? After all, you say that the FORTRAN call's
parameters are perfectly compatible with the PASCAL procedure's header
--  even  though  there's an OPTION CHECK 3  test, it should be passed
with flying colors.

   Well,  if  you  look  in  chapter  9  of your  System Tables Manual
(what??? you say you don't have a System Tables manual???), you'll see
the following paragraph:

   "PASCAL:  Pascal  sets  the  high  order bit in  the parameter type
    descriptor  when it is generating  hashed values. The remaining 15
    bits  are based on a hash of  the types of the parameter. Only the
    Pascal  compiler  can  compute  the value, and  the SEGMENTER must
    match the whole 16 bit value."

Since  PASCAL has so many  different types (RECORDs, enumerated types,
subranges,  etc.), the PASCAL compiler  generates a special "parameter
type  entry" into the procedure's OPTION  CHECK 3 descriptor, an entry
that is INCOMPATIBLE WITH THE ENTRIES GENERATED BY ANY OTHER COMPILER,
even  if  the  parameter  types  referred  to by PASCAL  and the other
compiler are perfectly compatible.

   In  other  words,  you  can't  call  (with  OPTION CHECK  3) from a
non-PASCAL  program any OPTION CHECK 3 PASCAL procedure. What you must
do is either:

   * make sure that the PASCAL  procedure is NOT compiled with OPTION
     CHECK 3, or

   * make  sure  that  the  calling  program  does  not  generate any
     procedure calls with OPTION CHECK 3.

   PASCAL has two compiler keywords:

   * $CHECK_ACTUAL_PARM$,  which  indicates  the  checking  level for
     PROCEDURES YOU CALL, and

   * $CHECK_FORMAL_PARM$,  which  indicates  the  checking  level for
     PROCEDURES YOU ARE DECLARING.

   FORTRAN, on the other hand, has only one relevant compiler keyword:

   * $CONTROL   CHECK=,  which  indicates  the  checking  level  for
     PROCEDURES YOU ARE DECLARING.

   What  this means is that FORTRAN will ALWAYS GENERATE ITS PROCEDURE
CALLS  WITH OPTION CHECK 3, since there's no keyword to turn this off.
Therefore,   you   must   change  your  PASCAL  procedure  to  have  a
$CHECK_FORMAL_PARM 0$.

   What  if you don't have the source code to the PASCAL procedure? In
this  case,  you're in trouble, since the  procedure has check level 3
and  FORTRAN will always generate  external procedure calls with check
level  3. To avoid this, you have  to write an SPL "gateway" procedure
which  calls  the  PASCAL  procedure  and  is  called  by  the FORTRAN
procedure.  If the SPL gateway procedure declares the PASCAL procedure
to be OPTION CHECK 0, EXTERNAL, this will work.

   For  the curious: Yes, there are check levels 1 and 2. OPTION CHECK
1 indicates checking of only the PROCEDURE RESULT TYPE; OPTION CHECK 2
indicates  checking  of  the  PROCEDURE RESULT TYPE  and the NUMBER OF
PARAMETERS.  OPTION  CHECK  3,  as  I  mentioned  before,  checks  the
PROCEDURE  RESULT TYPE, the NUMBER OF PARAMETERS, and the TYPES OF ALL
THE PROCEDURE PARAMETERS.


Q: I notice that MPE lets me allocate extra data segments that belong
to a process (call GETDSEG with id = 0) or a shared within a  session
(GETDSEG  with id <> 0).  I want to have a global data segment that's
shared among many sessions.  I'd like to be able to have one  program
that  loads  this  segment up from a file or from a database (say, at
the beginning of the day), and all the other ones will then read data
from the segment without having to go to disc.  How can I do this?

A: Unfortunately,  you  can't  share  data  segments  among  sessions
without  doing  some  serious privileged mode work.  However, are you
sure you really want to have an extra data segment?

   Whenever people talk about accessing files, they think "disc I/O".
After all, files are stored on disc, right? Well, almost right.  When
you read a disc file (in default, buffered  mode),  the  file  system
doesn't actually go out to disc for every FREAD; rather, it reads one
disc  BLOCK  at  a time (however many RECORDS it may contain) into an
in-memory buffer.  Whenever the record to be read is already  in  the
buffer,  it's  taken  from the buffer rather than from disc.  This is
then just a memory-to-memory move, rather like a DMOVIN.

   Now, if you want to keep your data in a  data  segment,  you  must
have  no  more  than  32K  words  of  data (actually, slightly less).
Curiously enough, this also happens to be close to the  maximum  size
of a file system buffer.  If you say

   :BUILD X;REC=128,255

then  each  block  in  X  will be built with 255 records of 128 words
each.  When you first read the file, the file system  will  read  all
32640  words  of it into memory; any subsequent reads of the file (as
long as they fit within those 128 records) will require NO  DISC  I/O
AT  ALL,  since  they'll  be  satisfied  entirely  from the in-memory
buffer.

   Thus, what you need to do is:

   * Build the file with a sufficiently high block size (record  size
     WILL FIT IN ONE BLOCK.

   * Always open the file SHR (shared) GMULTI  --  the  GMULTI  means
     that  everybody  will  share THE SAME BUFFER (rather than having
     one 32K data segment for each file accessor!).

   Then, all of your reads will be (almost) as fast as  data  segment
accesses,  since  that's exactly what they'll be! The only difference
is that you'll be letting the file system take care of all  the  data
segment management behind your back.

   In  fact,  using  files  in this case will give you a lot of other
advantages (besides inter-session sharing) over data segments:

   * You can use your  language's  built-in  file  access  mechanisms
     instead  of  having  to call DMOVIN, DMOVOUT, GETDSEG, FREEDSEG,
     etc.

   * If you want to look at your file to make sure that it's correct,
     you can use FCOPY, EDITOR, etc.

   * If people will be updating the data segment, you can  use  FLOCK
     and  FUNLOCK  to coordinate the write access to it.  (Make sure,
     however, that nothing  you  do  relies  on  the  current  record
     pointer -- see below!)

   * If the system goes down, the file will still remain around.

   In  general,  files  are just a lot easier to deal with than extra
data segments, and as long as you block them right (and  stay  within
32K,  which  is  the limit for a data segment anyway), they can be as
fast or almost as fast!

   The only thing you need to beware of is that when you open a  file
GMULTI,  not  only  the  file  buffers  are shared among all the file
accessors, but so are the current record pointers! In other words, if
two people have  a  file  opened  GMULTI  and  both  are  reading  it
sequentially,  one  will get about half of the file's records and the
other will get the rest! As soon as one reader reads record  #0,  the
current record pointer (which both readers share) will be incremented
to  1,  and the other reader will read record #1 and never see record
#0.

   Therefore, whenever you're reading (or  writing)  a  GMULTI  file,
DON'T  DO  SEQUENTIAL  READS (unless you do some sort of locking, for
reads as well as  writes).   Do  all  your  I/O  using  FREADDIR  and
FWRITEDIR (or their equivalents in your language).


Q:  Sometimes,  when  I  do  a :LISTF of a file, I get a display like
this:

   FILENAME  CODE  ------------LOGICAL RECORD-----------  ----SPACE----
                     SIZE  TYP        EOF      LIMIT R/B  SECTORS #X MX

   URLDAT    USL     128W  FB         141        959   1       60  2 32

The file has 529 records, but uses only 300 sectors!   Is  this  some
super compression algorithm HP is using? Can I make all my files this
small?

A:  Sorry, no.  This file uses only 300 sectors because it  has  less
than  300  records  worth of actual data.  Although there is a record
#528 (which is why the EOF is  529),  some  of  the  records  between
record #0 and record #528 are missing!

   How  can  this happen? Well, try it yourself.  Build a file with a
command such as

   :BUILD X;DISC=1023,8

-- a file with 1023 records and up to 8 extents.  The first of  these
extents will be allocated when you build the file (except on MPE/XL);
the others will be unallocated until you write to them.

   Now, write a small program that does an FWRITEDIR of a record into
record  #1022.   Then, when you :LISTF the file, you'll see something
like:

   FILENAME  CODE  ------------LOGICAL RECORD-----------  ----SPACE----
                     SIZE  TYP        EOF      LIMIT R/B  SECTORS #X MX

   X                 128W  FB        1023       1023   1      256  2  8

Only the first extent and the last extent (the one with record #1022)
will actually be allocated -- the others will remain empty until  you
actually write to them.

   Once you think about it, it's all pretty straightforward -- you
write a record to an extent and that extent gets  allocated;  if  you
don't  write  a record to an extent, it won't get allocated.  However,
when you first see this, it can be puzzling indeed!

   In fact, this situation is quite rare, since  most  people  either
write  to  files  sequentially or entirely allocate the whole file to
start with (e.g.  IMAGE databases, KSAM  key  files,  and  most  data
files).   The  SEGMENTER  is one of the few subsystems that regularly
creates "holey" files; it's quite common to find USL files like this.


Q: There's a program that I run that HAS to have a temporary file  of
its  own  to do its work -- it just couldn't possibly work otherwise.
However, when I hit [BREAK] and do a :LISTFTEMP, MPE says  there  are
no  temporary  files;  :LISTF doesn't show me any new permanent files
either.  Where is the program keeping all  its  temporary  data?   It
doesn't  have  DS  capability,  so  I  know it's not in an extra data
segment.  I'm not quite clear on the  distinction  between  permanent
and  temporary  files,  anyway.   Are they just kept in two different
places?

A: If you want a file to be available  to  sessions  other  than  its
creating  one  (either at the same time or later), you must make it a
PERMANENT file.  The system will then keep its name and a pointer  to
its data in the PERMANENT FILE DIRECTORY, a large chunk of disc space
reserved for this very purpose.  :LISTF, of course, scans through the
directory,  as  does  FOPEN  when  you  ask  it to access an existing
permanent file.

   If you create a file within your own session and know that  you'll
never need it outside your session, you can make it a TEMPORARY file.
Its  name  and  data  pointer  will  be  kept  in  the TEMPORARY FILE
DIRECTORY, an extra data segment (actually part of the JDT)  kept  by
the  system  on  your  session's  behalf.   (Naturally,  there is one
Temporary File Directory for each session in the system, but only one
Permanent File Directory for the  entire  computer  [ignoring  for  a
moment private volumes].)

   Temporary  files  have  two  major advantages: first, they go away
when your session dies (and their disc space is released) -- this can
be convenient, since otherwise you'd have to remember to  purge  them
before logging off (naturally, you'd always forget).

   More  importantly,  since  each session has its own Temporary File
Directory, you  need  not  be  afraid  of  interfering  with  another
session's  temporary  files.   If  your program needs to build a work
file (which you want to call WORKFILE) and two people are running  it
in  the  same  group, then you have the possibility of both processes
trying to create WORKFILE at the same time.  If you keep WORKFILE  as
a temporary file, each session will have its own WORKFILE file in its
own Temporary File Directory.

   Temporary  files  also  have  two  major  disadvantages.   One, of
course, is the very fact that they are temporary  --  if  the  system
crashes or the job aborts, the files will be lost; there'll be no way
of  seeing  what  was  in  them.   If one of your job temporary files
contains important information, this may be a serious concern  --  if
your  job aborts, the file will be lost, and you won't be able to see
what was in the file (which might give you some important clues as to
why the job aborted).

   The other problem is that when the system crashes, the space  used
by the temporary files is lost.  Since the Temporary File Directories
are  all  kept  in  extra  data  segments,  the  information  in them
(especially the pointers to the file data) will be lost as well,  and
thus  the system will not be able to re-use the space occupied by the
temporary files until you do a RECOVER LOST DISC SPACE.

   However, as your question points out, there are more  things  than
just  permanent  and  temporary  files.   "Permanent" and "temporary"
indicate what DIRECTORY the file name and the  pointer  to  the  file
data  are  kept  in  --  what if they don't need to be kept anywhere?
What if a file will only be needed while a program is running, and as
soon as the program closes it, it can safely be discarded?   In  this
case,  you  wouldn't  need to keep pointers to it in any permanent or
even semi-permanent place -- only in the file tables that  MPE  keeps
in your own stack.

   When  you  call  FOPEN (or when it's called on your behalf by your
language's compiler library), you can tell FOPEN which  directory  to
look  for the file in.  You can tell it to look in the PERMANENT FILE
DIRECTORY -- this means that the file must exist as a permanent  file
at  FOPEN  time;  you  can  tell  it  to  look  in the TEMPORARY FILE
DIRECTORY -- this means that the file must exist as a temporary  file
at FOPEN time.  Finally, if you don't expect the file to exist at all
but  rather  want  to create a new file, you can indicate that in the
FOPEN call, too.

   When you call FOPEN to open this "new file",  the  file  will  NOT
actually  be  cataloged  in either directory -- this will only happen
when you FCLOSE the file (you can tell FCLOSE  whether  to  save  the
file  as permanent or temporary).  As long as the file is FOPENed but
not yet FCLOSEd, the file exists (space is allocated for it on disc),
but it's not pointed to by ANY directory.  If the process FCLOSEs the
file without asking FCLOSE  to  save  it,  the  file  will  go  away;
similarly,  if  the process dies without FCLOSEing the file, it'll go
away as well.  The file is thus a "process temporary file",  in  that
it  is  accessible  only  by  the process that created it (unless the
process later saves the file) -- in the manuals, it's usually  called
a  "new  file", meaning not just a recently created file but rather a
file that is not kept  track  of  in  either  the  Permanent  or  the
Temporary File Directory.

   This  is  what  is  almost  certainly  happening  in  your mystery
program.  It needs to store data somewhere, but  sees  no  reason  to
save  it  for  access by other sessions or even by other processes in
the same session.  It FOPENs the file as NEW,  and  then,  when  it's
done  with  the  data, either FCLOSEs the file (without saving it) or
just terminates.  Neither :LISTF (which looks in the  Permanent  File
Directory)   nor  :LISTFTEMP  (which  looks  in  the  Temporary  File
Directory) will show this file, because the file is  utterly  unknown
to anybody other than the creating process.

   We can summarize all this with the following table:

                PERMANENT FILE      TEMPORARY FILE     NEW FILE

                --------- ----      --------- ----     --- ----
Available to    Anyone, any time    Only creating      Only creating
                                    session            process

Shown by        :LISTF              :LISTFTEMP         Nothing

If process      File remains        File remains       File destroyed,
  aborts                                               space recovered

If job aborts   File remains        File destroyed,    File destroyed,
                                    space recovered    space recovered

If system       File remains,       File destroyed,    File destroyed,
  crashes       space not wasted    space lost         space lost


Q: I recently did a :SHOWME in one of my batch jobs, and I  got  some
rather unusual output:

USER: #S329,TEST.PROD,PUB            (NOT IN BREAK)
MPE VERSION: HP32033G.B3.00.  (BASE G.B3.00).
...
$STDIN LDEV: 4          $STDLIST LDEV: 5

I  don't  have ldevs 4 and 5!  I expected it to say "$STDIN LDEV: 10"
(since 10 is my :STREAMS device) and "$STDLIST LDEV: 6" (since  6  is
the  only device in my LP class), but it gave me those rather bizarre
values instead.  What's going on here?

A: Actually, if you did have ldevs 4 and 5 configured, you would  NOT
have  gotten  them  in  your :SHOWME output.  In fact, a :SHOWME in a
spooled (i.e. :STREAMed to a spooled printer) batch job is GUARANTEED
to give you nonexistent $STDIN LDEV and $STDLIST LDEV numbers!

   Why is this?  Well, in the depths of the  "System  Operations  and
Resource Management" manual (p. 7-92 of my January 1985 edition), you
can find an interesting statement:

   "When  a  spool  file is opened, MPE creates a 'virtual device' of
    the required type by filling in an unused  logical  device  entry
    with the appropriate values."

What does this mean?

   Well, say that you open a spool file on device class LP.  At first
glance,  you  might think that the device number associated with this
file would be 6, the device of  the  line  printer.   Actually,  this
clearly  can't  be  so  -- when you write a record to the spool file,
device 6 doesn't actually get written to; rather, the spool  file  on
disc  is  written  to and is then (when the file is closed) copied to
device 6.

   OK, you think, that's right -- the file is on disc; therefore, its
device number must be 1, 2, or 3  (the  device  numbers  of  my  disc
drives),  depending  on  which  disc  drive  it happened to be built.
Nope.  For some reason, the operating system requires that the  spool
file  be  more than just a simple disc file.  Perhaps this is because
some MPE I/O (e.g. the CI's prompt for the command) goes through  the
internal ATTACHIO procedure rather than through the file system; the
ATTACHIO call requires a logical device number.

   In any event, whenever you open a spool file, the system assigns a
"virtual  logical  device  number" to it, and most access to the file
then happens through this device;  naturally,  this  virtual  logical
device  number must not be the same as any real device number.  Since
all batch jobs (except those  that  are  submitted  from  non-spooled
devices,  such  as  tapes and card readers, or those that are sent to
non-spooled devices, such as hot printers) have spool files both  for
their  $STDINs and $STDLISTs, all batch :SHOWMEs and WHOs will return
these virtual (i.e. invalid) logical device numbers.

   If you're curious about  getting  the  REAL  $STDIN  and  $STDLIST
devices,  call  the  new  JOBINFO intrinsic asking for items 9 (input
device) and 10 (output device).  The :SHOWME command should really do
this, but it was written long before JOBINFO was implemented.


Q: My program opens a file with Input/Output access, does some  reads
from  it,  some  writes  to  it,  and then closes it.  When I run the
program it works well, but when one of  my  users  runs  it,  nothing
happens.   The  program  doesn't  get a file open error, but the file
doesn't get updated, either.   I  thought  it  might  be  a  security
violation, but the file open's succeeding, so that can't be it.

A:  Well, maybe it can.  One would think that if you don't have write
access to a file, an open for Input/Output would fail with  an  FSERR
93  (SECURITY  VIOLATION); however, this is not so.  If you have read
access but  NOT  write  access  and  you  try  to  open  a  file  for
Input/Output  (or  Update), the FOPEN will SUCCEED; however, the file
will be opened for READ ACCESS ONLY.

   Now, if you try to write to the file, the FWRITE will  fail  (with
an  FSERR  40,  OPERATION  INCONSISTENT  WITH  ACCESS  TYPE).  If you
checked for a file system error after the FOPEN but you DON'T checked
for a file system error after the FWRITE, everything will have looked
OK; but, since the FWRITE failed, the file won't have been updated.

   What can you do?  Well, you could (and should) check for an  error
after  the  FWRITE  call;  if,  however, you want to detect the error
condition at FOPEN time, you have to do something like this:

   FNUM:=FOPEN (FILENAME, 1 << old >>, 4 << in/out >>);
   IF <> THEN
     FILE'ERROR
   ELSE
     BEGIN
     FGETINFO (FNUM, , AOPTIONS);
     IF AOPTIONS.(12:4)<>4 THEN
       << you asked for IN/OUT access, but got something less >>
     ...
     END;

The FGETINFO call will get you the REAL aoptions that  the  file  was
opened  with  --  then you can check to see if the access granted was
really IN/OUT.

   Moral of the story (#1): ALWAYS check for error conditions.

   Moral of the story  (#2):  Sometimes  a  successful  result  isn't
really successful.


Q:  I have a program that runs just fine when I run it normally (with
$STDIN not redirected).  However, when I try to redirect  its  $STDIN
to  a  disc  file, it aborts on MPE with a tombstone that says "ERROR
NUMBER: 42".  I looked it up, and the  manual  says  it's  "OPERATION
INCONSISTENT WITH DEVICE TYPE (FSERR 42)".  What does that mean?  The
program doesn't use any special devices (tapes, printers, etc.).

A:  Well,  actually  the  program  DOES  use a special device -- your
terminal.  When you run your program with its $STDIN redirected to  a
disc  file,  all  of  its  file  system  operations  on $STDIN become
operations on a disc file (rather than on a  terminal).   For  normal
FREADs,  that's  OK  -- you can read from a disc file just as well as
from a terminal.  But what if the program does an FCONTROL mode 13 on
$STDIN to turn off echo?  Or an FCONTROL mode 14 to turn  off  break?
These  operations work when passed a terminal file; however, when you
pass them a disc file (even if its your program's  ;STDIN=),  they'll
fail with the very error condition you indicated.

   Oddly  enough,  if your program is doing, say, an FCONTROL mode 13
to turn off echo, then the file system error is actually no  problem.
The  FCONTROL  13 is only useful when $STDIN is a terminal; if it's a
disc file, the FCONTROL can't be done, but it isn't  needed,  either.
It  may  be  that  your  program  is  seeing  this error and aborting
although it would be perfectly OK for it to go on.

   If that's what you're doing -- an FCONTROL mode 13, or perhaps one
of the other FCONTROLs that's only needed when you're really  reading
from  the  terminal  --  then  you  may  want  to  check for an error
condition, and if it's FSERROR 42,  keep  going  as  if  nothing  had
happened.   Or  --  dare I suggest it -- maybe not even check for the
FCONTROL error at all?

   Moral of the story: Sometimes an error really isn't an error.


Q: I'm doing a "SQUEEZE" of a disc file (i.e. releasing allocated but
unused disc space beyond the EOF) with an MPEX %ALTFILE  ;SQUEEZE;  I
understand  that  this  just  opens  the  file  and  closes  it  with
disposition 8.  This works for all my fixed record length  files  and
some  of  my  variable  record length files, but some variable record
length files it leaves completely untouched.  Their FLIMITs and  disc
space usages remain exactly the same as before.  What's happening?

A:  Well,  I was stumped until I looked at my trusty MPE source code.
After I saw the actual code that did the "squeeze",  everything  fell
into place.

   Once  upon  a  time  (before  MPE IV came out), FCLOSE mode 8 only
worked for fixed record length files.  This is because its job is  to
release  all the unused blocks beyond the end of file -- for this, it
has to know what the last used block is.  In a  fixed  record  length
file, the last used block # (counting from block #0) is equal to

   CEILING (eof/blockingfactor) - 1

because every block has exactly blockingfactor records.

   In a variable record length file, however, there can be any number
of  records  in a block.  The EOF (the number of records in the file)
won't tell you what the last block # is; the only  way  to  find  the
last block (in MPE III) was to read the file sequentially  until  the
EOF was reached -- way too slow for FCLOSE 8 to use.

   MPE IV allowed you to  append  to  variable  record  length  files
(perhaps  because  message  files,  which  are always variable record
length, needed to be appended to).  To do an append, the file  system
also  needs  to  know  the  last  block  #; for this, the file system
started keeping the last used block # in every variable record length
file's file label.  This incidentally made FCLOSE mode 8s of variable
record length files possible.  Thus, in MPE IV and later systems,  an
FCLOSE  mode  8  of  a variable record length file simply goes to the
"end of file block number" and throws away all  the  blocks  that  go
after it.

   So  why  do  some  FCLOSE 8s succeed and others do nothing?  Well,
what if the file system sees an end of file block number  of  0?   It
could  mean  a  file  with exactly 1 block (block #0) -- OR, it could
mean a file that was first created on a pre-MPE IV system,  when  the
end  of  file  block# field was ALWAYS set to 0!  FCLOSE 8 can't just
throw away all blocks after block  #0,  since  there  MAY  be  (on  a
pre-MPE IV file) a lot of data in those blocks.

   Therefore,  you  have  a  bit  of  a paradox.  Any variable record
length file that has at least  two  data  blocks  will  get  properly
squeezed;  however,  any  file that's very small -- has only one data
block -- won't be modified.  In this isolated case, the smaller files
may actually use more space (when squeezed) than the larger ones!


Q: Is there an easy way to determine  what  language  a  program  was
written in?  Maybe some word in word 0 of the program file?

A:  Nothing  that direct, I'm afraid.  Code is code -- a program file
is just a set of an assembly instructions; there's no  need  for  the
operating system to inquire into where they came from, so MPE doesn't
keep this information around.

   However,  if  there  isn't  a  direct  way,  there  may well be an
indirect one.  In fact, there are two -- neither is certain, but they
work for most programs.

   The first method is based on the fact that all compilers -- except
SPL -- generate calls to  so-called  "compiler  library"  procedures.
For instance, when you do a WRITELN in a PASCAL program, the compiled
code won't actually contain all the instructions that do the I/O, nor
even a direct call to the FWRITE or PRINT intrinsic; rather, the code
will  have  a  call to the P_WRITELN procedure, a system SL procedure
that actually does the PASCAL-file-to-MPE-file mapping, the I/O,  the
error  check,  etc.  All languages (except SPL) rely on such compiler
library procedures, typically to  do  I/O,  but  also  to  do  bounds
checking,  implement  certain  complicated  language constructs, etc.
(Even SPL has a few "compiler library" procedures of its own that  it
calls -- anybody know what they are?)

   The  point here is that each language has its own compiler library
routines -- PASCAL's typically  start  with  "P'"  (e.g.  P'WRITELN),
COBOL's   with   "C'"  (e.g.  C'DISPLAY),  BASIC's  with  "B'"  (e.g.
B'PRINTSTR).  You'd think that FORTRAN's would start with  "F'",  but
they don't -- the most common ones are FMTINIT', SIO' (and in general
xxxIO'), and BLANKFILL'.

   Armed  with  this  knowledge,  you can just run the program with a
;LMAP parameter.  This will show you  all  the  external  references,
which,  in  addition  to  those  intrinsics that you explicitly call,
should include a whole bunch of compiler  library  procedures.   From
the names of these procedures, you can figure out the source language
of  the  program  (if  there are no such procedures, it's probably in
SPL).

   Note that it is possible, for instance, for a FORTRAN program  NOT
to  call any compiler library procedure (if it does no formatting and
nothing else that requires any compiler  library  help)  --  however,
it's quite unlikely.

   The  other  way  of figuring out a program's source language is by
using the fact that all languages (except for SPL) do  some  sort  of
automatic  I/O error checking.  If they encounter an unexpected error
condition, they will abort with tombstones -- each one with  its  own
kind.  The trick is to force an I/O error; for a program that's doing
character mode I/O, the easiest way is by typing a :EOD on $STDIN.

   * Most  FORTRAN  programs  that do an ACCEPT or READ (5,xxx) will
     abort  with  "END  OF  FILE  DETECTED  ON  UNIT  #  05"  and   a
     PRINTFILEINFO tombstone for file "FTN05".

   * Most  COBOL  programs will print an error message such as "READ
     ERROR on ACCEPT (COBERR 551)" and then do a QUIT.  The "COB"  in
     the "COBERR" should clue you in.

   * Most PASCAL programs will abort with an error message like "****
     ATTEMPT TO READ PAST EOF (PASCERR 694)".

   * Most BASIC programs will abort with an "**ERROR 92: I/O ERROR ON
     INPUT  FILE"  and  then a BASIC tombstone, which is similar to a
     PRINTFILEINFO but different.  The second line of  the  tombstone
     will probably be "FNAME: BASIN".

   This  is  a  less  reliable trick since it won't work for programs
that do no direct terminal input (e.g. ones that do no terminal input
at all or do it through V/3000); also, some smart programs  may  trap
input errors and handle them themselves, rather than just letting the
compiler library abort.

   Finally,  note  that  these  techniques  will probably work for HP
Business Basic, RPG, and maybe even C programs -- all  of  them  will
probably  have their own compiler libraries, and all of them (except,
perhaps, C) will have their own way  of  aborting  on  an  input  I/O
error.


Q:  I  have about 30 job streams that  need to run over the weekend. I
want  them all to run in sequence, not because each one depends on the
previous  ones, but because each is  so CPU-intensive that running two
at  once would bring the system to its knees (and completely drown out
any  other  users  that  might  be  foolish  enough to try  to use the
computer at the time).

   At  first,  I  tried  to  set  the  job  limit  to  1.  This proved
impractical  in  my environment, since other  users may want to submit
their  own  jobs while the 30 sequential  jobs are running. (The other
users'  jobs can't use ;HIPRI to bypass  the job limit since the users
don't have SM or OP.)

   The  next thing I tried was the old trick of having each job stream
submit  the  next  job  stream  at  the  end  of its  execution -- for
instance, JOB1 might look like:

   !JOB JOB1,USER.PROD;OUTCLASS=,1
   ...
   !STREAM JOB2
   !EOJ

JOB1  streams  JOB2,  JOB2  streams JOB3, etc.  Unfortunately, if I do
this,  then any job that aborts in the middle would prevent any of the
other  jobs from running, which is  unacceptable. I thought of putting
!CONTINUEs in front of every line in each job stream, but I WANT a job
stream  error  to flush the remainder of  THAT job stream (but not the
others). Putting !CONTINUEs and then !IFs designed to prevent (in case
of error) the execution of everything EXCEPT for the final !STREAM was
also  unreasonable  -- my job streams are  hundreds of lines long, and
would thus require hundreds of !CONTINUEs and nested !IFs.

   Another  idea I had was to have  each job stream SCHEDULE (not just
stream)  the  next  job at the BEGINNING (rather  than the END) of the
job. Thus, JOB1 might look like:

   !JOB JOB1,USER.PROD;OUTCLASS=,1
   !STREAM JOB2;IN=,3
   ...
   !EOJ

The  first  line  in  JOB1  schedules JOB2 to run  in 3 hours; if JOB1
aborts, JOB2 would already have been scheduled.

   This  one  almost worked; unfortunately, the  run times of the jobs
vary  greatly. If JOB1 runs more than the interval time (in this case,
3 hours), then JOB1 and JOB2 would have to run simultaneously, and the
system  would  grind to a halt. If,  however, to counteract this I set
the  interval time even higher, then the jobs might just take too long
to  run  --  even at 3 hours per job,  the 30 jobs would take 90 hours
(almost  4  days!).  I don't want to have  any unused time between job
executions.

   (Actually,  I only mention this for completeness' sake -- I have an
old  Series  III  and  am running MPE V/R,  which does not support job
scheduling.)

   Finally,  there's one other alternative I can think of. I can write
a  program that wakes up every several minutes, does a :SHOWJOB into a
disc  file,  sees  if  one  of  my  jobs is running,  and if none are,
:STREAMs the next one (it would keep track of what the next one should
be).  Then  I  would  stream  this "traffic-cop" program  and it would
submit my 30 jobs.

   Unfortunately,  I  don't  relish the task  of writing this program,
having  it  analyze  the  :SHOWJOB output, execute  MPE commands, open
files, etc. I'd like to make do with the minimum possible programming.
Is  there  an "MPE Programming" solution  to this problem that doesn't
require  writing  a  custom program that I'd then  have to rely on and
maintain?

A:  Wow!   You  sure  are  tough  to please! When  I read your opening
paragraph,  I thought of all four  of the solutions that you proposed,
but  it seems that none of them is good enough for you. Fortunately, I
did  manage to come up with another solution, though it took some hard
thinking.

   Let's look at what you're trying to achieve. You want JOB2 to start
up  as  soon  as JOB1 finishes (no sooner  and no later), whether JOB1
finished  successfully or aborted. What  mechanism currently exists in
MPE for doing this sort of thing?

   Well,  there's  no  way of doing EXACTLY  this; however, there is a
similar  feature  not  for  jobs, but for files.  If JOB1 has an empty
message file opened and JOB2 is waiting on an FREAD against this file,
then  JOB2  will  awaken as soon as JOB1  closes the file, whether the
close  is done normally or as the result of a job abort. Thus, if JOB1
said

   !JOB JOB1,...
   !OPEN MSGFILE FOR WRITE ACCESS
   !STREAM JOB2
   ....
   !EOJ

and JOB2 said

   !JOB JOB2,...
   !READ MSGFILE
   ...
   !EOJ

then  JOB2  would  try  to  read MSGFILE (presumably  an empty message
file),  see that it's empty, and wait  until a record is written to it
OR  until  all  of MSGFILE's writers close  the file. JOB1 has MSGFILE
opened for write access as long as it's running; when JOB1 terminates,
MSGFILE will automatically be closed, and JOB2 will automatically wake
up.

   Sounds  good? It's very much what we want, except for two things --
MPE  has  no  way  of opening a file or  of reading a file. The "!READ
MSGFILE" can actually be done by saying

   !FCOPY FROM=MSGFILE;TO=$NULL

Unfortunately, there's really no documented way for your job to open a
message  file and keep it open. A program  run by the job can open the
message  file, but as soon as the program terminates, the file will be
closed;  what  you  want  is  to have the  Command Interpreter process
itself  to open the file. But, since the CI has no !OPEN command, this
is impossible, right?

   Of course, this (opening a file in the CI and keeping it opened) IS
possible.  It just can't be done DIRECTLY, but must instead be done as
a SIDE EFFECT of something else.

   The CI often opens files -- the :PURGE command opens the file to be
purged,  :RENAME opens the file to be renamed, :LISTF and :SHOWCATALOG
open  their list files. However, after opening the file, the CI (being
a  well-written program) closes it. If we want to subvert a CI command
so  that  it  will  open  a file but not close  it, we have to somehow
inhibit the close operation.

   One  way that comes to mind is to somehow make the CI's FCLOSE fail
with  some  sort  of file system error. If  the FCLOSE fails, the file
remains opened. Our plan would be to issue some sort of :FILE equation
that would prevent the FCLOSE from succeeding.

   There  are very few ways in which the FCLOSE of an already-existing
file  can  fail.  One  little-known  way  is  if  you try  to FCLOSE a
permanent  file  with  disposition  2,  which means "save  the file as
temporary". If you try to do this, you'll get file system error 110,

   ATTEMPT TO SAVE PERMANENT FILE AS TEMPORARY  (FSERR 110)

If  the  FCLOSE  fails, the file is  not closed, and therefore remains
open. Thus, we can say:

   :BUILD MSGFILE;MSG
   :FILE MSGFILE,OLD;TEMP
   :PURGE *MSGFILE

Sure enough, we get the error

   ATTEMPT TO SAVE PERMANENT FILE AS TEMPORARY  (FSERR 110)
   UNABLE TO PURGE FILE *MSGFILE. (CIERR 385)

Now,  we  do a :LISTF of the file, and  we see that... the file has no
asterisk ("*") after its name! Although the FCLOSE failed, the file is
NOT  STILL  OPENED.  The :PURGE command was  smart enough to see that,
since the FCLOSE failed, it should do a special FCLOSE that guarantees
that the file will get closed no matter what.

   So,  what now? Did I lead you all the way through this mess just to
let  you  down  in  the end? Hold off on  those nasty letters! Try the
following:

   :BUILD MSGFILE;MSG
   :FILE MSGFILE,OLD;TEMP
   :SHOWCATALOG *MSGFILE

Now, do a :LISTF MSGFILE,2 -- the file is still open! The :SHOWCATALOG
command  (almost  alone  of  all the MPE commands)  does NOT force the
close of its list file; if the FCLOSE fails (as a result of the wicked
:FILE  equation  we set up), the list  file will remain open until the
job or session that did it logs off!

   Therefore, here's the solution:

   !JOB JOB1,...
   !BUILD MSGFILE;MSG
   !FILE MSGFILE,OLD;TEMP
   !SHOWCATALOG *MSGFILE
   !STREAM JOB2
   ...
   !EOJ

   !JOB JOB2,...
   !FCOPY FROM=MSGFILE;TO=$NULL
   !FILE MSGFILE,OLD;TEMP
   !SHOWCATALOG *MSGFILE
   !STREAM JOB3
   ...
   !EOJ

   !JOB JOB3,...
   !FCOPY FROM=MSGFILE;TO=$NULL
   !FILE MSGFILE,OLD;TEMP
   !SHOWCATALOG *MSGFILE
   !STREAM JOB4
   ...
   !EOJ

   Oh,  yes, don't forget to put a  few !COMMENTs in those job streams
that  explain  what  you're  doing  -- I wouldn't want  to be the poor
innocent programmer who has to figure out what those !SHOWCATALOGs are
for!

   In  any  event,  this  is  just  what  you  asked  for  --  an "MPE
programming" solution that meets all your criteria AND doesn't require
a  specially-written program that does PAUSEs, SHOWJOBs, etc. The only
problem  -- and it's potentially a very  serious one -- is that you're
relying  on  an  MPE BUG (:SHOWCATALOG's not  closing of its list file
when  the  normal FCLOSE fails) which in  theory could be fixed at any
time.

   However,  if you're using MPE V/R, you needn't worry about too many
operating  system improvements. And, if you can ever ditch your Series
III  and  get  a  REAL  MACHINE, you might not  have such horrible CPU
resource problems.

   In  any  case,  this  is  yet another example  of what amazing (and
somewhat  disquieting) things that you can do with a bit of ingenuity.
I'll  bet  you  never  thought that the  :SHOWCATALOG command could do
something like this!


Q:  I  have  a  temporary file that I'd like  to /TEXT and /KEEP using
EDITOR. I wish that EDITOR had a /TEXT ,TEMP command and a /KEEP ,TEMP
command for doing this, but it doesn't. I tried saying

   :FILE MYFILE;TEMP

and then saying

   /TEXT *MYFILE

and

   /KEEP *MYFILE

-- the /TEXT worked but the /KEEP didn't. I then tried saying

   :FILE MYFILE,OLDTEMP

which  also  let the /TEXT work, but  still didn't do the /KEEP right.
Why doesn't this work? What can I do?

A:  A  :FILE  equation's  job is to alter the  way a file is opened or
closed.  Therefore,  to  understand  a  :FILE equation's  effects on a
particular  program, you have to understand  how the program opens and
closes the file to begin with.

   First  of  all,  what does EDITOR do when  you say /TEXT MYFILE? It
FOPENs  the  file  not  as an OLD file  (which would mean setting bits
(14:2)  of  the  foptions  parameter  to  1)  or  as  an  OLDTEMP file
(foptions.(14:2)  :=  2),  but  with  foptions.(14:2)  set to  3. This
special  FOPEN  mode  tries to look for the  file first as a temporary
file,  and  then  if the temporary file  doesn't exist, as a permanent
file. When you say

   /TEXT MYFILE

EDITOR  will first try to text in  the temporary MYFILE file, and then
(if  there  is  no temporary MYFILE) as  a permanent MYFILE; thus, you
don't  have  to do anything special to  /TEXT in a temporary file. You
didn't need either a :FILE MYFILE,OLDTEMP or a :FILE MYFILE;TEMP.

   Now, what happens when EDITOR does a /KEEP MYFILE? It actually does
four things:

   1.  It tries to FOPEN MYFILE with foptions.(14:2)=3 (look first for
      a temporary file, then for a permanent file).

   2. If this FOPEN succeeds (i.e. MYFILE already exists), it asks you
      "PURGE  OLD?",  and if you say  yes, FCLOSEs it with disposition
      DELETE (to purge the old file).

   3.  It FOPENs MYFILE as a new file  (and then writes the data to be
      /KEEPed to it).

   4.  It  FCLOSEs  MYFILE  with  disposition  SAVE  (to save  it as a
      permanent file).

Assume, then, that there are no file equations in effect. If you don't
have  a  temporary  file  (or  permanent file) called  MYFILE, a /KEEP
MYFILE would:

   1. Try to FOPEN the permanent or temporary file MYFILE -- and fail.

   2. FOPEN a new file MYFILE and write data to it.

   3. FCLOSE MYFILE with SAVE disposition (as a permanent file).

Thus, the /KEEP MYFILE would merely build a permanent file.

   What  if  MYFILE  already  exists as a temporary  file and you do a
/KEEP MYFILE (again without :FILE equations)? EDITOR would:

   1. FOPEN the temporary file MYFILE.

   2. FCLOSE it with DELETE access (thus purging it).

   3. FOPEN a new file MYFILE and write data to it.

   4. FCLOSE MYFILE with SAVE disposition.

Thus,  you'd  still  build MYFILE as a  permanent file, but you'd also
lose  the  temporary  file  MYFILE  in  the  process!  (Quite strange,
actually,  since EDITOR could easily have  saved MYFILE as a permanent
file without touching the temporary file.)

   Now,  what  if  there  is BOTH a temporary  file named MYFILE and a
permanent file named MYFILE? Here's what EDITOR would do:

   1. FOPEN the temporary file MYFILE.

   2. FCLOSE it with DELETE access (thus purging it).

   3. FOPEN a new file MYFILE and write data to it.

   4.  FCLOSE MYFILE with SAVE disposition -- which would fail because
      there's already a permanent file called MYFILE!

In  this  case,  EDITOR purged the WRONG  FILE -- purged the temporary
file INSTEAD of the permanent file; you've lost the temporary file and
still haven't done your /KEEP, since the permanent file still exists.

   Confused yet? This is what happens WITHOUT a :FILE equation!

   Now,  let's say that you do a :FILE MYFILE,OLDTEMP when there is no
temporary (or permanent file) named MYFILE. This is what would happen:

   1.  EDITOR  tries  to  open  the  file MYFILE (as  a temporary file
      because of the :FILE equation). The FOPEN fails because the file
      doesn't  exist, but that's no big deal, since EDITOR expected it
      to fail if the file didn't exist.

   2.  EDITOR  now  tries  to  open  the file MYFILE as  a new file --
      however,  the  :FILE  equation  overrides  this,  and  the FOPEN
      actually  tries to open the file  as an OLDTEMP file! This FOPEN
      fails,  and EDITOR prints "*41*FAILURE TO OPEN KEEP FILE (53) --
      NONEXISTENT TEMPORARY FILE (FSERR 53)".

If a temporary file called MYFILE existed, you wouldn't have much more
luck,  since  by the time the second  FOPEN took place, the file would
have been deleted by the first FCLOSE.

   Finally, let's say that you do a :FILE MYFILE;TEMP when a temporary
file named MYFILE already exists. Then, when you /KEEP *MYFILE, EDITOR
will:

   1.  Try  to  FOPEN MYFILE as an old  permanent or temporary file --
      this  FOPEN would succeed quite  well, opening the old temporary
      file.

   2.  Ask you whether you want to "PURGE  OLD?" -- if you say YES, it
      will  try  to FCLOSE the file  with DELETE disposition. HOWEVER,
      the ;TEMP on the :FILE equation (which says "close the file as a
      temporary  file")  will override the DELETE  -- the file will be
      closed, but not deleted!

   3. FOPEN MYFILE as a new file and write all the data to it.

   4.  Try to FCLOSE the file  as a permanent file (SAVE disposition).
      The  :FILE  equation's  ;TEMP will override  this, so the FCLOSE
      will try to close the file as a temporary file, which is exactly
      what  you  wanted.  However,  remember  that  the  purge  of the
      temporary  file  that we tried to do  in step #2 actually wasn't
      done!  Thus,  the  FCLOSE  ;TEMP  will  fail  with  a "DUPLICATE
      TEMPORARY  FILE  NAME  (FSERR  101)". You said  "YES" when asked
      "PURGE  OLD?",  but  that  never  happened because  of the :FILE
      equation.

   Those are the problems you've been running into. If this all sounds
complicated,  that's  because it is. To understand  it, you have to be
keenly  aware of every FOPEN and  every FCLOSE that EDITOR does. Since
these  FOPENs  and  FCLOSEs  actually aren't  documented anywhere, you
really have to guess at what EDITOR must be doing.

   What's  the solution? Well, let's see what  it is that we WANT each
FOPEN and FCLOSE to do:

      WHAT EDITOR DOES                 WHAT WE WANT IT TO DO

   1. Open MYFILE as old               Open MYFILE as old
      permanent or temporary           temporary (so if there's
                                       an old permanent MYFILE
                                       but no old temporary
                                       MYFILE, the old permanent
                                       MYFILE won't be purged)

   2. Close MYFILE with DELETE         Close MYFILE with DELETE
      disposition                      disposition

   3. Open MYFILE as a new file        Open MYFILE as a new file

   4. Close MYFILE with SAVE           Close MYFILE with TEMP
      (save as permanent file)         (save as temporary file)
      disposition                      disposition

In  other  words,  we want OPEN #1 to  be affected by a :FILE ,OLDTEMP
without  having  the same :FILE equation affect  OPEN #3; and, we want
CLOSE #4 to be affected by a :FILE ;TEMP without having the same :FILE
equation affect CLOSE #2.

   What single :FILE equation can we use to do this? The simple answer
is: there isn't one. Instead, we must do two things:

   /:PURGE MYFILE,TEMP

to delete any old file named MYFILE and then

   /:FILE MYFILE;TEMP
   /KEEP *MYFILE

to  /KEEP MYFILE as a temporary  file. Since the temporary file MYFILE
is  then already gone, the :FILE ;TEMP will not badly affect FCLOSE #2
(the  one  that's supposed to do the  purge of the old temporary file)
because FCLOSE #2 will not actually be done (since FOPEN #1 would have
failed since the old temporary file did not exist). Instead, the :FILE
equation   will   influence  FCLOSE  #4,  which  will  then  save  the
newly-built  /KEEP file as a temporary file rather than as a permanent
file.

   Thus, that's your solution:

   /TEXT MYFILE              << no file equation needed >>
    ...
   /:PURGE MYFILE,TEMP
   /:FILE MYFILE;TEMP
   /KEEP *MYFILE

   The only problem is this: what happens if there is both a permanent
and  a  temporary file named MYFILE? Will  the /KEEP then succeed? The
answer to this question is left for the reader...


Q: I'm trying to decide whether I should do my future development in C
or  PASCAL.  The one great advantage of PASCAL,  I am told, is that it
does  much more stringent type checking; I understand that this can be
more  of a deficiency than an advantage, but I've heard that PASCAL/XL
lets you optionally waive type checking when needed. I think that type
checking  (as  long  as  it  can be avoided when  necessary) is a very
valuable  thing;  it's much better to let  the compiler catch the bugs
than make the programmer find them all.

   However, Draft ANSI Standard C seems to do the converse -- it seems
to bring C's type checking up to a reasonable level (just as PASCAL/XL
brought  PASCAL's  type checking DOWN to  a reasonable level). Is this
true?

A:  Like  many  good answers, the answer to  this question is "yes and
no".  First,  though,  let's  talk  a  moment  about  "classic"  (i.e.
pre-Draft ANSI Standard, also known as "Kernighan & Ritchie") C.

   K&R  C did virtually no type checking at all. For example, say that
the  function  "func" took two integer  parameters. Your program could
easily say

   func (10)

and  thus pass "func" only one parameter; of course, the results would
be  quite  unpredictable  (and almost certainly  undesirable), but the
compiler  wouldn't  say  a  thing. Similarly, you could  try to pass 3
parameters by saying

   func (10, 20, 30)

Again,  the  compiler will be perfectly happy  to do this, even though
it's  a  pretty  obvious bug (one that  will probably result in "func"
getting  the  wrong  values  and in garbage being  left on the stack).
Finally, you might also say:

   func (10.0, 20.0)

--  try  to  pass  two floating-point numbers  (rather than integers).
Since  in  most C implementations floating  point numbers use twice as
much  space as integers, this will again badly confuse both the caller
and the callee, but the compiler will not complain.

   Other  examples  of  this  total  lack  of parameter  type checking
abound.  C  requires  you  to  pass  all "by-reference"  parameters by
specifically prefixing each one with an "&", e.g.

   func (&refvara, &refvarb);

If  you  omit the "&" when it's needed  (or specify it when it isn't),
the  compiler  won't  catch the error; needless to  say, if you try to
pass a record of type A when the procedure expects a record of type B,
the compiler won't catch this, either.

   As  you  see, this can get pretty  unpleasant. God knows, all of us
make  mistakes; I, for one, would  much rather have the compiler catch
them  for me. This may be "protecting the programmer against himself",
but  that's  a  good  idea  in  my  book. We programmers  need all the
protection we can get, especially against ourselves.

   Draft  ANSI  Standard  C  --  which  more and more  C compilers are
implementing -- is much better. It lets you define so-called "function
prototypes", which really are declarations of the parameter types that
each function expects. For instance, if you say

   extern int FFF (int, int, float *);

then  the  compiler will know that FFF  is a function that takes three
parameters -- two integers, and a float pointer (i.e. a real number by
reference) -- and returns an integer. Then, if you try to say

   float x;
   int i, j;
   i = FFF (10, 20);                or
   i = FFF (10, 20, &x, 30);        or
   i = FFF (10, 20, x);             or
   i = FFF (10, 20, &j);

then  the compiler will catch all four errors (too few parameters, too
many, not-by-reference, and bad type). There are (fortunately) ways to
waive  type  checking  for  all calls to a  particular function, for a
particular  function parameter, or for a particular function parameter
in  a  particular  function call, but in  the overwhelming majority of
cases in which type checking is useful, it's available.

   So,  to  answer  your  question  --  is Draft ANSI  Standard C type
checking  now  as  good  as PASCAL/XL's? In my  opinion, I'm afraid it
isn't.

   First  of  all,  remember the old "equal  sign" problem? One of the
things  that  has  always bedeviled me about  C is that C's assignment
symbol is "=" and its comparison symbol is "==". Thus, saying

   if (i=5) ...

does  NOT  check  to  see if I is equal to 5  -- it assigns 5 to I and
checks  to  see  if  the result (the assigned  value) is non-zero. Try
finding  a bug like this some time  -- it certainly isn't easy! People
have  told me "oh, you'll get used to  it"; I worked with C on and off
for years, and I did NOT get used to it.

   Now,  many  C  programmers  will tell you that  one has no right to
complain about this; after all, it's a fairly arbitrary decision which
symbol is to be used for which function -- C had just as much right to
use "=" and "==" as PASCAL had to use ":=" and "=".

   However,  the key problem is not  C's choice of symbols, but rather
the  C compiler's reaction to a programmer  error! If you mixed up the
symbols in PASCAL and said something like

   IF I:=5 THEN

then  the PASCAL compiler would complain, since it doesn't recognize a
":="  inside  an  IF  condition. Even if it  did (which would be quite
reasonable), it would still print an error, since the result of "I:=5"
would  be  an  integer  and  the  IF statement would  expect a boolean
(logical) value.

   Unlike  C, PASCAL has a distinction between the integer and boolean
data  types, and would thus be able to catch this simple mistake. If C
had the same distinction, it could look at the statement

   if (i=5) ...

and  realize that if the user really intended to have the IF condition
be the result of "i=5", this would be an error, since the result is an
integer rather than a boolean.

   Another  thing  that PASCAL/XL can do that  I suspect no Draft ANSI
Standard  C  compiler  can  do is bounds checking.  Say that you index
outside the bounds of an array in PASCAL/XL; the code generated by the
compiler  will  check  for  this  and promptly abort  the program. A C
program  indexing  outside  array bounds won't  abort unless the index
would  be outside the program's data  area altogether (which is rarely
the  case);  instead,  it  will either return  garbage data (if you're
reading), or overwrite random data if you're writing!

   In my experience, such bounds-error bugs have by all means been the
most  frustrating.  Often  they  don't  manifest themselves  until you
actually  try  to  use the accidentally  overwritten variable, usually
many  statements  after  the  actual error took  place. These bugs are
notoriously  hard  to  find,  and  the  compiler would do  you a great
service if it could help find them for you.

   To  the  best  of  my knowledge, no Draft  ANSI Standard C compiler
supports  bounds checking. However, this is  not just the fault of the
compiler  (I'm  sure  that  there  exist  some  PASCALs that  don't do
run-time  bounds  checking); rather, the  language itself makes bounds
checking  more  or  less  impossible. Whenever you pass  an array to a
procedure,  the procedure does not get any information about the array
bounds;  whenever you use pointers (which are used far more often in C
than  in  PASCAL), all possibilities for  bounds checking are lost. On
the  other  hand,  PASCAL  (and PASCAL/XL) is  designed very much with
run-time bounds checking in mind.

   Note  that,  of course, run-time bounds  checking exacts a price in
performance  (often a very substantial one). However, no-one says that
you  should be forced to use it; most PASCAL compilers, including both
PASCAL/V  and  PASCAL/XL let you turn it  off for production code that
you  want  optimized.  What's  important  is that you  should have the
OPTION of using it if you feel it's appropriate.

   Finally,  a third -- and much less serious -- incompleteness in C's
type  checking  is  that enumerated type constants  are viewed as just
another type of integer. For instance, if you say

   typedef enum {widget, gadget, thingamajig} item_types;

then you'll still be able to use "widget", "gadget", and "thingamajig"
interchangeably  with  integers  and with other  enumerated types. You
could say

   i = 10*gadget - widget;

(which  is meaningless if "gadget" and "widget" are really supposed to
be  symbols  rather  than integers); more  importantly, you can easily
accidentally compare a variable of a different enumerated type against
"gadget",  not realizing the difference  in types. The PASCAL compiler
would  catch this, and tell you that  it doesn't make sense to compare
objects of one enumerated type against objects of another, since their
numeric values are (in PASCAL/XL) purely accidental.

   So,  those are the major ways in which Draft ANSI Standard C's type
checking  is  less  powerful  (less  likely  to catch  your bugs) than
PASCAL/XL's:

   * No INTEGER vs. BOOLEAN distinction;

   * Most importantly, no run-time bounds checking;

   * No stringent enumerated type checking.

On  the other hand, remember that PASCAL/3000 (and other ANSI Standard
PASCAL's)  have  much  more severe flaws,  largely tending towards the
opposite extreme. A few examples are:

   * You  can't  have  a procedure that takes a  record or array of a
     variable type (you have to write one procedure for each different
     type -- so much for reusability of code);

   * You can't have a procedure  that takes arrays of different sizes
     (you have to write one procedure for each array size!);

   * You  can't  have  a procedure that is  actually intended to take
     varying numbers of parameters;

   * And more...

Where Standard C just makes it easier for you to make errors, Standard
PASCAL  makes  it  almost  impossible for you  to do certain perfectly
normal  and desirable things -- a much  graver sin in my book. Imagine
trying to write one matrix multiplication routine for each combination
of  array sizes! Or, imagine that you  wanted to write, say, a "shell"
procedure   for  the  DBGET  intrinsic  that  would  check  for  error
conditions,  possibly  call  DBEXPLAIN,  maybe log  something, etc. --
you'd  have to write one such  procedure for each possible record type
you might want to DBGET!

   However,  PASCAL/XL  seems  to  solve all (or  almost all) of these
problems  (as long as you're writing only for MPE/XL and never want to
retrofit your programs to bad old PASCAL/V!). Both Draft ANSI Standard
C  and PASCAL/XL seem to be very  nice languages, and much good can be
said  about both -- however, in the field of error checking, I believe
that PASCAL/XL is somewhat superior.


Q:   Whenever   I   run   SPOOK   from   QEDIT,  MPEX,  or  some  such
process-handling environment it seems to "suspend" when I exit it. I'm
not  exactly  sure  what  this  means;  it appears,  however that this
"suspension" makes the next :RUN of this program (from the same father
process)  faster, plus I have the same  file open (at the same current
line number) as I did when I exited!

Other  programs,  like QEDIT, SUPRTOOL, and MPEX  also do this; on the
other  hand,  QUERY  and  FCOPY don't. In  particular, I'd really like
QUERY  to  be able to suspend in this way,  since I want to be able to
exit  QUERY  and re-enter it with the  same databases opened, the same
records  found,  etc. Also, I'd like to be  able to do the same for my
own programs.

How  is this done? Is there some keyword (:RUN QUERY.PUB.SYS;SUSPEND?)
that  I can put onto my command to trigger this option? How much extra
resources does all this "suspending" cost me?

A:  Normally,  when a program decides that  it's done (e.g. the user's
typed  EXIT), it will call the TERMINATE intrinsic (either directly or
through  some  language  construct,  e.g.  STOP  RUN).  The  TERMINATE
intrinsic  will close all the process's  files, clean up any resources
(e.g.  locks,  data  segments,  etc.)  that the  process has acquired,
deallocate  the process's stack (which contains all its variables) and
then activate the process's father process.

   The  son  process  is  gone -- the only way  to reactivate it is to
re-:RUN  it  (or  use the CREATE or  CREATEPROCESS intrinsic to do the
:RUN  programmatically),  which  will  create  an  entirely  different
process, with its own files, variables, etc.

   For  many programs, this makes perfect sense -- when, for instance,
a  compiler is finished compiling, it  might very well decide that all
of  its  internal  files,  variables, etc. will be  of no more use; it
might as well re-activate the father process by TERMINATEing.

   However,  what  about  an interactive,  command-driven program like
SPOOK?  Its job is not to do  a specific, isolated task; rather, it is
to execute whatever commands you want it to do. When you type >EXIT in
SPOOK,  that might not mean that you  don't want to do any other SPOOK
commands;  it might just mean that you want to do something else for a
while  (e.g.  use  EDITOR, run QUERY, tc.), and  then will want to get
back into SPOOK.

   QUERY  is  an  even  more  aggravated  case -- what  if you've just
finished a 2-hour >FIND command, and have now discovered that you need
to look at your program to figure out exactly how to, say, format your
>REPORT.  You  want  to get out of  QUERY, but only temporarily; you'd
like  to go into your favorite editor,  look at your program, and then
come  back  to right where you left off,  in QUERY, with the big >FIND
waiting for you.

   Fortunately,  MPE  lets  you  have  more  than one  process in your
session. You can even have more than one ACTIVE process in the session
(which  can  get  very  complicated),  but you may  certainly have one
active  process and several SUSPENDED processes.  A son process can --
instead of calling TERMINATE -- merely activate its father and suspend
itself  (by  calling the ACTIVATE intrinsic  with the parameters 0 and
1).  Then,  the  son  process  and  all of its  files, variables, data
segments, etc. will stay alive, and the father can (at its discretion)
re-activate the son (using the ACTIVATE intrinsic). Not only will this
preserve the son's internal state, but also be a good deal faster than
re-:RUNing it.

   Thus SPOOK has chosen a rather smart solution -- by ACTIVATEing its
father,  it gives the father the  option of re-ACTIVATEing rather than
re-:RUNing  it  the next time the user  wants to get into SPOOK. Note,
though,  that  this  requires the cooperation OF  BOTH THE SON AND ITS
FATHER.

   If  the  son blindly TERMINATEs instead  of ACTIVATEing its father,
there's  nothing you can do; similarly,  if the father doesn't realize
that  the  son process is not dead (but  only sleeping), it may try to
re-:RUN  it  the  next  time it's needed, not  taking advantage of the
suspended son process that it already has. There's no ;SUSPEND keyword
on the :RUN command (or on the CREATE or CREATEPROCESS intrinsic) with
which a willing father can force an unwilling son process to suspend.

   This leaves three questions:

   * How can you make your  own program suspend whenever possible? In
     COBOL, you'd say:

        CALL INTRINSIC "FATHER" GIVING DUMMY-VALUE.
        IF CC = 0;
           THEN CALL INTRINSIC "ACTIVATE" USING 0, 1;
           ELSE STOP RUN.

     (where  CC was declared in the  SPECIAL-NAMES section to be equal
     to the special name CONDITION-CODE).

     The  FATHER intrinsic call (to which we pass DUMMY-VALUE, a dummy
     S9(4)  COMP)  will  check  to  see if our  father process was the
     Command  Interpreter or a user process.  If it was a user process
     (CC  = 0), we'll ACTIVATE it, thus suspending ourselves; however,
     if  it  was  a  CI (CC <> 0), the  ACTIVATE call will fail with a
     nasty-looking error, so we just do a normal STOP RUN instead.

     Of  course, after the IF, there should  be a GOTO back to the top
     of the prompt loop (or something like that); when your process is
     reactivated,   control  will  be  transferred  to  the  statement
     immediately  after the IF. It's  your responsibility to make sure
     that  when  you're awakened, you'll go on  doing what you want to
     do.

     Remember  that  it's also the  father process's responsibility to
     know  that  a  son process has suspended  and then re-activate it
     instead of re-:RUNing it. If your father process knows that a son
     process  will  suspend,  it  should  save  the son  process's PIN
     (Process  Identification  Number)  that CREATE  and CREATEPROCESS
     return,  and  pass  it  to  the  ACTIVATE  intrinsic  (using CALL
     "ACTIVATE"  USING  PIN,  2)  when  it's  time  for the  son to be
     re-activated.

   * What about QUERY? Is there anything you can do make it friendlier
     in this respect?

     For  normal QUERY, the answer  is, unfortunately, no. QUERY calls
     the TERMINATE intrinsic, and that's that. (If you got the idea of
     intercepting  the  TERMINATE  intrinsic  call and  making it call
     ACTIVATE  instead,  no dice -- this  will cause QUERY to suspend,
     but  when QUERY is reactivated, the very next instruction will be
     executed,  which will cause some rather unpleasant results. QUERY
     isn't expecting to get reactivated, so it's not properly prepared
     for this.)

     Note,  however,  that  if you use our  MPEX/3000, our "MPEX hook"
     facility  lets you make QUERY suspend (and much more!) -- give us
     a  call  at (213) 282-0420 if you  can't find "MPEX HOOK" in your
     manual.

   * Finally, what about performance?

     Well,  suspended  son  processes  do  consume virtual  memory and
     various  table  entries (DSTs, PCBs, etc.).  However, they do NOT
     consume any CPU time or any real memory (since if the memory they
     use is needed, they can be safely swapped out).

     On  the other hand, having your son  processes can save you a lot
     of  time just in TERMINATEs and :RUN alone (each takes some time,
     more  time than a simple ACTIVATE). Add to this the time you save
     re-opening databases and re->FINDing records in QUERY, re-opening
     databases  and forms files in  your own applications, re-/TEXTing
     files  in  EDITOR (which you had to  exit since you couldn't just
     suspend  it),  and  so  on  --  you'll find that  having your son
     processes  suspend can be much more computer-efficient as well as
     more efficient in using your own time.


Q: I have a very large program that I assemble from several USL files.
After  making a few additions, my :SEGMENTER -COPY commands started to
give me "ATTEMPT TO EXCEED MAXIMUM DIRECTORY SPACE" errors; apparently
I overflowed some internal space limitation.

The  funny thing is that I've seen  some program files that are bigger
than  mine (COBOLII.PUB.SYS, for instance, is 1802 records long, while
my  program  is  only  about  1600 records long), so  I know that it's
possible to have programs that large. What can I do?

A:  Every  USL  file contains two types  of information: directory and
non-directory.  The non-directory information  includes all the actual
machine  instructions generated by the compiler; every word of code in
your program takes up at least one word of non-directory information.

The  directory  information contains only  the directory of procedures
that  your  program  contains:  one  variable-length  entry  for  each
procedure.  Although  this  will almost certainly take  up a good deal
less  space  than the non-directory information,  it is also much more
limited  --  in  fact, you can have at  most 32,767 words of directory
information in any single USL file.

This  is  an  ABSOLUTE limit. It's just not  possible to fit more than
32,767  words  in  the USL directory (because  the SEGMENTER keeps all
directory pointers in 15-bit fields).

How  can you decrease the amount of USL directory space you use? Well,
one  approach is to break the USL file up into several files. This can
be done in one of several ways:

   * Break the program up into several program files which communicate
     via  process handling. Naturally, this  will only work if there's
     some  reasonable dividing line in the  program -- perhaps if each
     program  performs  several distinct tasks that  do not share much
     code.

   * Move some procedures into an SL file. This will work as long as:

       -  The  procedures  do  not  use  any  global  storage  (global
         variables,  SPL  OWN  variables, FORTRAN  COMMON areas, etc.;
         FORTRAN formatting operations are also forbidden in SLs).

       -  All  the  procedures  you  move into the  SL call only other
         procedures in this SL (or system intrinsics); there's no easy
         way  of  calling  from the SL back  to your program. In other
         words,   you'll   have  to  separate  your  program  into  SL
         procedures  and non-SL procedures; the SL procedures can then
         only  call other SL procedures, but the non-SL procedures can
         call either non-SL or SL procedures.

   * Move  some  procedures into an RL file.  RL files can, unlike SL
     files,  use  global  variables; however, they're  limited in some
     other ways:

       - An RL file can have at most 127 entry points (i.e. procedures
         or secondary entry points).

       - An RL file can contain at most 16,383 words of code.

       -  Like  SLs,  any  code you move to the  RL must not call code
         that's outside of the RL.

     An  RL  can  thus  only give you a bit  of breathing room; it can
     decrease your USL directory size by up to 127 entry points' worth
     of  space;  at about 20 words per  entry, this can save you about
     2,500 words out of the 32,767 maximum.

The  best solution will probably be (in most cases) to move procedures
into  an  SL  file.  RL  files  can save you only  a little space, and
splitting  up a program will often  be impossible when all the various
parts of the program share a good deal of code.

Fortunately,  there  are  also a few ways  to minimize directory space
usage directly, without splitting your USL file. What exactly does the
directory contain?

   * It  contains  one  entry per PROCEDURE,  one entry per SECONDARY
     ENTRY POINT, and one entry per SEGMENT.

   * Each procedure entry (the most common kind) contains 14 1/2 words
     of fixed overhead, plus

       - the procedure name,

       -  one extra word if your procedure is OPTION CHECK 1 or OPTION
         CHECK 2,

       - one word per parameter if your procedure is OPTION CHECK 3,

       - one word for each procedure this procedure calls,

       - one word for each global variable this procedure references,

       - and various other (less important) things.

Some  of  these  things  you can't easily control  -- you could try to
minimize  the number of procedures each  procedure calls, but it would
be quite hard. Other things, fortunately, are not so tough:

   * The easiest one is the space used for each procedure if they are
     compiled  with OPTION CHECK 1, 2, or (especially) 3. OPTION CHECK
     allows the SEGMENTER to make sure that a procedure is called with
     the  correct number of parameters  and parameter types; in PASCAL
     and  SPL  this  essentially means that  the SEGMENTER will ensure
     that  the  actual  procedure declaration and  its OPTION EXTERNAL
     declaration are identical.

     This  is  quite valuable if you  have many different source files
     that  you compile into one program; it  can prevent a lot of very
     strange  bugs that might occur  because of incompatible procedure
     declarations.  However, to support this, the compiler must insert
     information  on  all procedure parameter types  into the USL, and
     that takes up USL directory space.

     By  default, FORTRAN and PASCAL compile all their procedures with
     OPTION CHECK 3, the most thorough but most directory-space-hungry
     method.  Saying $CONTROL CHECK=0 in FORTRAN or $CHECK_FORMAL_PARM
     0$ in PASCAL will switch to the more efficient OPTION CHECK 0.

     In  SPL, the default is OPTION  CHECK 0; however, some people use
     OPTION  CHECK 3 for the extra  error-checking it provides. I used
     to do this myself, but found I had to switch back to OPTION CHECK
     0 to save space.

   * You can try to shorten your procedure names. Long procedure names
     are  very  good  because  they're  much  more  readable; however,
     circumstances  may  force  you  to  compromise on  this point. If
     you're  running  into  the  USL  directory space  limitation, you
     probably  have  about  1,000 procedures; if  you can shorten each
     procedure name by two characters, you can save 1,000 words, which
     may mean the difference between success and USL overflow.

   * Finally,  if  you're  realy  hard  up,  yuo can  combine several
     procedures  into  one.  This  would  be  a  shame  -- one  of the
     fundamental  rules  of  good  structured  programming is  to make
     procedures  as small as possible -- but,  as I said, you may have
     to compromise your principles on this one.

So  there are your alternatives. Incidentally, this should explain why
larger  program files than yours can exist even though you've run into
the USL file limit -- the USL directory size is more a function of the
number of procedures you have than of your total code size.

You  can take some comfort from the fact that your problems prove that
you  have  a  good programming style; you must  obviously use a lot of
relatively  small  procedures, which is usually  a good thing -- until
you run into the USL directory size limit.

Oh,  yes, there's another alternative -- buy a Spectrum, which doesn't
have this limitation in Native Mode. Good luck.


Q:  Help! Sometimes the user friendly HP3000 can surely be unfriendly.
I  really  did  not  think that what I wanted to  do was so far out of
line. But, as much as I try, I can't seem to get the HP to cooperate.

Here  is  the situation. I have a job  that logs onto my A system (I'm
lazy,  so I just named my computers A, B, C, D, and E). It then has to
remotely  log on to my B system  and remotely run a program (Robelle's
SUPRTOOL  in  this  case).  So far, everything works.  Now, here is my
problem: how can the job tell if the remotely run program aborts?

I've tried the following:

   !JOB MGR.OPR
   !REMOTE HELLO MGR.OPR;DSLINE=B
   !REMOTE RUN SUPRTOOL.PUB.ROBELLE
   !IF JCW>=FATAL THEN
   !  TELLOP It aborted!
   !  EOJ
   !ENDIF
   !EOJ

Of  course, since I did a REMOTE RUN,  the remote JCW would be the one
that  was  set,  not  my  local  JCW.  This way, my  IF would never be
executed.


Then, I tried:

   ...
   !REMOTE RUN ...
   !REMOTE IF ...
   !  TELLOP It aborted!
   !  EOJ
   !ENDIF
   ...

But,  since  I used a REMOTE IF, the  TELLOP and the indented EOJ were
always executed.

Finally, I came up with a method that almost works:

   ...
   !REMOTE HELLO MGR.OPR;DSLINE=B
   !REMOTE REMOTE HELLO MGR.OPR;DSLINE=A
   !REMOTE RUN ...
   !REMOTE IF ...
   !REMOTE   REMOTE TELLOP It aborted!
   !REMOTE ENDIF
   ...

Now,  the REMOTE IF checks the REMOTE RUN and the REMOTE REMOTE TELLOP
tells  the  operator  on  A  that the program on  B aborted (since the
REMOTE  REMOTE  goes  back  to A, the local  system). However, I can't
figure out how to cause the rest of the job to flush.

Is  there  a  solution  or  will  HP  someday  have to give  us way to
interrogate JCWs that are on a remote computer?

A: Yes, there is a solution, and here it is.

Your  problem  is  that you want to  communicate information from your
remote  machine  (machine  B) to your local  machine (machine A). This
information  is  stored  in a JCW, so the  most logical way for you to
access it would be if HP let you access remote JCWs, e.g.:

   ...
   !REMOTE HELLO MGR.OPR;DSLINE=B
   !REMOTE RUN ...
   !IF B:JCW >= FATAL THEN
   !  TELLOP ...
   !  EOJ
   !ENDIF
   ...

Unfortunately,  this  is  easier  said  than  done. There's  no way to
directly access JCWs across DSLINEs.

However,  there  is  one sort of object  that can be relatively easily
accessed  across  DSLINEs  --  a  file.  The  trick is  to convert the
information  stored in a JCW into  "file form", access the file across
the  DSLINE, and then, so to speak,  convert the file back into a JCW.
Think of it as a sort of modem for JCWs!

What you should do is:

   * Depending on the value of the JCW, you can build a file across a
     DSLINE, e.g. by saying:

          !PURGE FLAGFILE
          !REMOTE RUN SUPRTOOL.PUB.ROBELLE
          !REMOTE IF JCW<>0 THEN
          !REMOTE   COMMENT Build a file on machine A (the local one)
          !REMOTE   BUILD FLAGFILE;DEV=#
          !REMOTE ENDIF

   * Now, you either have (if JCW<>0) or don't have (if JCW=0) a file
     called  FLAGFILE on system A. At  this point, you should "convert
     the file back into a JCW":

          !SETJCW CIERROR=0
          !CONTINUE
          !LISTF FLAGFILE;$NULL
          !IF CIERROR=0 THEN
          !  COMMENT File exists, remote run must have aborted.
          !  PURGE FLAGFILE
          !  TELLOP It aborted!
          !  EOJ
          !ENDIF

     If FLAGFILE exists, then the !LISTF FLAGFILE;$NULL will leave the
     CIERROR  JCW  set  to 0; the !IF will  see that CIERROR=0, send a
     message  to  the  console,  and  terminate  the job.  If FLAGFILE
     doesn't  exist (i.e. all went well), the !LISTF will fail and set
     CIERROR to 907.

As  you see, we've converted the information carried by the JCW (which
can't  be  accessed  across  DSLINEs) into information  carried by the
presence  or absence of a file (which CAN be accessed across DSLINEs).
Then,  on the local machine, we've converted the presence/absence of a
file backed into a JCW using a !LISTF.

It's kinky, but it'll work.

Note  that  the :REMOTE BUILD X;DEV=# might  not work in certain DS/NS
configurations  (in  particular,  when the local  machine is a DS/3000
machine  and the remote is an NS/3000  machine). In this case, you can
just  build  the  flag file on the remote  machine, switch back to the
local  machine, and then use DSCOPY to  try to copy the flag file from
the  remote to the local. You can  then check the DSCOPY result to see
if the flag file existed.

The  principle  would  still  be  the  same  --  use the  existence or
non-existence of the flag file to record the failure or success of the
remote program.


Q:  I have a small program that's supposed  to do only a few things --
an  FWRITE or two and a couple of DBGETs. I'd think that it should run
pretty  fast;  even  if each FWRITE takes one  disc I/O and each DBGET
takes  as many as three (and I understand that shouldn't happen unless
my  master dataset is really full),  that shouldn't be more than about
10  I/Os. At 30 I/Os per second,  that should take less than a second;
and,  since my program is :ALLOCATEd, I imagine that it won't take too
long to load it.

Unfortunately,  it seems that whenever the program is run, it takes at
least  about  five  or  ten seconds. This may not  seem like much of a
problem,  but  I  want to run it fairly  often, at logon time and from
inside  virtually every UDC that my users use (the program is supposed
to  do some user activity logging). Why is it taking so long? Should I
change my files' blocking factors? My master datasets' capacities?

A:  Only an FWRITE or two and  a couple of DBGETs? Really? Isn't there
something you must be doing BEFORE doing those FREADs and DBGETs?

Every  file you FWRITE must be  FOPENed; every database you DBGET from
must  be DBOPENed. An FWRITE might take  30 milliseconds if a disc I/O
is required and 3 milliseconds if a disc I/O is not required; an FOPEN
will   take   about  300-500  milliseconds.  A  DBGET  might  take  30
milliseconds  (even  if you have synonym  chain trouble, you might get
away with one disc I/O); a DBOPEN can take up to 2 seconds!

I  kicked all the other users off my MICRO/3000 XE, turned off caching
and did some tests. What I came up with was:

   FOPEN of a vanilla disc file        0.3-0.5 milliseconds

   FOPEN of a KSAM file                about 1.2 seconds

   DBOPEN of a database that           about 2 seconds
     nobody else has open

   DBOPEN of a database that           about 0.8 seconds
     is opened by somebody else
     (TurboIMAGE only)

   First DBGET from a dataset in a     about 0.4-0.5 seconds
     database that nobody else has
     open

   First DBGET from a dataset in a     about 50 milliseconds
     database that somebody else has
     open

If  nobody  else  has  your  database  open and you  do DBGET from one
dataset  and an FWRITE from one disc file, you're talking about 2.7 to
3  seconds in DBOPEN and FOPEN time alone! Of course, caching will cut
this  time  down  by  some unpredictable amount,  but competition from
other users will drive it up by some (probably larger) amount.

Note that the time taken by the actually database or file read will be
about 0.06 seconds.

What  can  you do? For starters, you  can minimize the number of files
you  open.  Possibly  you  open  some  files "just in  case" and don't
actually  use  them; possibly you can  restructure your application to
combine  several files into one. Perhaps instead of writing to several
files,  you  can write one record to a  message file that will then be
processed  by  a  background  job  --  this  job  can  then  write the
appropriate  records,  having FOPENed the files  once its life, rather
than once for each file.

If  you  use  TurboIMAGE,  you can take advantage  of the fact that it
takes  less  time  to  DBOPEN  a database that  is already DBOPENed by
somebody  else  -- you might have a  background job stream that simply
keeps  the  database  open.  Also, since each  TurboIMAGE dataset is a
separate  disc  file,  the  first  access  to  a  dataset  will  force
TurboIMAGE  to FOPEN the disc file (another 0.3 to 0.5 seconds) UNLESS
the  dataset  is  already "globally opened". This  will be the case if
somebody  else has the database DBOPENed  and has already accessed the
dataset, thus opening it.

It's  even better if somebody has  opened the dataset for WRITE access
(by  doing a DBUPDATE, DBDELETE, or DBPUT to it), since if the dataset
is  opened for READ access and you try to do a write to it, the system
will  still have to re-FOPEN the dataset  for you, not once but twice!
(Another 0.6 to 1 seconds.)

FOPEN  and  DBOPEN  are  slow simply because they have  to do a lot of
work. An FOPEN has to:

   * Find  the  file  in  the directory (about 3  or more I/Os on the
     average).

   * Read the file's file label from disc.

   * Write the file label to disc to reflect the fact that the file is
     open (various file label fields have to be changed).

   * Possibly  expand the opening process's  stack to accommodate the
     additional file table entries -- this might take another disc I/O
     if there's not much room in main memory.

   * If the file is buffered, possibly allocate an ACB (Access Control
     Block)  data segment to keep the file buffers -- this might again
     require a disc I/O to flush something out of memory.

A DBOPEN has to do all this just to open the root file; then it has to
allocate various control data segments, read the root file tables into
them,  update  various root file fields, etc.  This means a lot of CPU
processing and a lot of disc I/O.

FOPEN  and  DBOPENs take a long time  (and FCLOSEs and DBCLOSEs take a
while,  too).  Though it's easier said  than done, avoid them whenever
possible.


Q:  Tell me one more time -- how can I erase a master dataset? I tried
the  obvious  thing  (DBGET mode 2, DBDELETE,  DBGET mode 2, DBDELETE,
etc.),  and,  sure  enough,  I found some records  were left. I recall
hearing  something  some time about  "migrating secondaries", but I've
forgotten  it  (and,  to be frank, I never  quite understood it in the
first place). What's the good word?

A:  There are two things that need explaining: what the problem is and
what the solution is. First, the problem.

The  simplest  way  of  erasing a dataset is, as  you said, to read it
serially  and  delete every record you read.  For a detail dataset, it
will  work just fine; but for a master, it won't, because WHENEVER YOU
DELETE  A  RECORD, IT'S POSSIBLE THAT  A DIFFERENT RECORD WILL MIGRATE
INTO  ITS  PLACE.  When you read the  next record, you'll have already
skipped the migrated record, and thus you won't ever delete it.

Let's draw a little picture:

   ---------
   |   A   |
   ---------
   |   B   |
   ---------
   |   C   |
   ---------
   |   D   |
   ---------
   |   E   |
   ---------
   |  ...  |
   ---------

You read record A and delete it; you read record B and delete it; now,
you  read  record  C and delete it.  However (for reasons I'll explain
shortly),  when  you  delete C, record F  gets migrated into the place
that record C just occupied. Now, the file looks something like this:

   ---------
   | empty |
   ---------
   | empty |
   ---------
   |   F   |   <-- current read pointer
   ---------
   |   D   |
   ---------
   |   E   |
   ---------
   |  ...  |
   ---------

Since  you've  just  deleted  record  C, you get  the next record (D),
delete  it,  get  the next record (E), delete  it, and so on; however,
you'll NEVER delete F, since it's managed to work its way into the are
you think you've already cleared.

Why  did  F  migrate  into C's slot? Well,  master datasets are HASHED
datasets, in which a record's location is determined by the hash value
of  its key. "C"'s hash value was 3,  so C ended up in the 3rd record;
whenever we want to find C, we just hash it, get the value 3, and know
that we should look in the 3rd record.

However,  it's possible that several keys may hash into the same value
-- in our case, C and F both hashed to 3. Clearly we can't put both of
them  into the same record, so we put C into record 3, put F into some
other  location,  and  put a pointer into record  3 that points to the
place where F is. F is now considered a "synonym" of C -- both C and F
are  part of a "synonym chain", a  linked list of records that hash to
the  same value. C is the "primary" because it's actually put into the
location  indicated  by  the hashing algorithm (record  3); F (and all
other things that hash to 3) are called "secondaries".

Note  that F, being a secondary, takes  longer to access. To DBGET the
record  C,  we just get the hash value  (3), and look at record #3; to
DBGET  the  record  F,  we have to look at  record #3, realize that it
isn't  the one we're looking for, and  then go look the next record in
the synonym chain. Accessing a primary takes only one I/O; accessing a
secondary can take longer.

Thus,  when  we DBDELETE C, IMAGE decides  to be "smart" -- instead of
leaving F as a slow-to-access secondary, it MIGRATES F into record #3,
thus  making F a primary. Now, whenever we try to DBGET F, you can get
to  it in one I/O. Unfortunately, this "migration" prevents the record
from being deleted with our straightforward algorithm.

What  can we do? Well, the trick is to realize that after you delete a
record,  there may now be another record in the slot from which you've
just  deleted.  By  looking at words 5 and  6 of the STATUS array that
DBGET returns you, you can tell if the record you've just deleted is a
primary,  and  if it is, how long  the synonym chain is (including the
primary). Then, after you DBDELETE the primary, you can know not to do
another DBGET mode 2 (which will skip over the migrated secondary) but
instead  do  more  DBDELETEs  until  you've  deleted all  the migrated
records.

Thus, you'd say:

   WHILE not end of dataset DO
     BEGIN
     DBGET mode 2;
     IF STATUS(words 5 and 6) = 0 THEN
       << not a primary, just delete it >>
       DBDELETE
     ELSE
       FOR I:=1 UNTIL STATUS(words 5 and 6) DO
         DBDELETE;
     END;

If  we  run  into  a  secondary,  we  delete it  without any migration
concerns  (since migration only happens when  we delete a primary); if
it's  a  primary,  we  do  not  just  one DBDELETE but  rather as many
DBDELETEs as there are entries on the synonym chain (1 if there's only
the  primary,  2  if  there's  one  secondary, etc.). If  there are 10
records  on  a  synonym  chain  (one plus 9  secondaries), each of the
secondaries  will  migrate  into  the  primary spot as  the primary is
deleted;  thus,  it'll take us exactly 10  DBDELETEs to get rid of the
entire  chain. (The doubleword stored in STATUS  words 5 and 6 will in
this case be 10.)

So, there you go -- for each record you DBGET, you may have to do more
than  one DBDELETE! I strongly suggest that  you put a LOT of comments
in this piece of code when you write it.


Q:  Every  year,  twice  a year, I get really  messed up by this whole
Daylight  Savings  Time  thing. It's hard enough  to change all my own
clocks and watches, but we also have to change the system times on our
dozen HP3000s.

Invariably,  every  year  somebody forgets, and  we don't realize what
happened  until  a  whole  bunch  of  data  entry  records  have  been
time-stamped  incorrectly;  even if we remembered  to do everything on
Monday  morning, the changeover happens on  Sundays, so any user who's
working on Sunday will be in trouble.

Can't  my computer do this for me? I understand that some UNIX systems
have these calculations built-in -- why can't the HP3000?

A:  Computers  can do everything! (Well,  almost.) Here's what you can
do:

   * First of all, you need to make sure that you have the contributed
     unsupported  CLKPROG utility on your  system (I believe that it's
     available in TELESUP) -- if you don't, get it quickly!

   * Then, you have to be able to figure out WHEN the changeover will
     take  place so you can schedule a job to run then. You would most
     likely  want  to  figure  this out from some  nightly job you run
     (e.g. your system backup).

   * Finally, have a job that runs  CLKPROG (which prompts for a date
     and  time),  feeding  it  the  correct  adjusted  date  and  time
     automatically.

When  does Daylight Savings Time come into  effect? I called the U. S.
Government  reference  information  number  (I'm  telling you  this so
you'll  blame  them,  not me, if this is  wrong) and they said that we
"spring  forward"  one hour at 2 AM  (which instantly becomes 3 AM) on
the  first Sunday in April and we "fall back" at 2 AM (which instantly
becomes 1 AM) on the last Sunday in October.

Let's  say  that,  every  Friday,  you  do  a backup  that runs before
midnight.  How can you tell if the upcoming Sunday is the first Sunday
in  April? Well, the first Sunday in April has to fall between 1 April
and  7 April; if today is a Friday  between 30 March and 5 April, then
we know that the next Sunday will be "spring forward".

Similarly,  the last Sunday in October  must be between 25 October and
31  October;  thus,  if  today  is a Friday between  23 October and 29
October, then the next Sunday will be "fall back".

So, your Friday night backup job should say:

   !IF HPDAY=6 AND ((HPMONTH=3 AND HPDATE>=30 AND HPDATE<=31) OR
                    (HPMONTH=4 AND HPDATE>=1 AND HPDATE<=5)) THEN
   !  STREAM DLITEON;DAY=SUNDAY;AT=2:00
   !ENDIF
   !IF HPDAY=6 AND HPMONTH=10 AND HPDATE>=23 AND HPDATE<=29 THEN
   !  STREAM DLITEOFF;DAY=SUNDAY;AT=2:00
   !ENDIF

(Note  that  we  check that HPDAY is actually  6, i.e. that the job is
actually  running  on Friday, just in case  the job is actually run on
some  other  day.  This algorithm can easily  be adapted to jobs that,
say, run on Saturday -- all that's necessary is that there be SOME job
that  is  guaranteed to run shortly  before the changeover should take
place.)

Now,  what  about  running  CLKPROG?  CLKPROG  (at  least  my version,
A.00.01) prompts for the date, in MM/DD/YY format, and for the time --
how  do you supply the new date and  time to it? You'd like to be able
to say:

   !JOB DLITEON,MANAGER.SYS;OUTCLASS=,1
   !RUN CLKPROG.UTIL.SYS
   SAME
   3:00
   !EOJ

to  tell CLKPROG to keep the same date  and to change the time to 3:00
AM;  however, CLKPROG demands that you  explicitly specify a new date,
even if it's the same as today's date.

What can you do? You have to use "MPE programming" techniques to build
a  ;STDIN=  file  that you can then pass  to CLKPROG. The idea is that
we'll somehow do a :SHOWTIME into a disc file, use EDITOR to translate
the  :SHOWTIME output into CLKPROG input,  and then feed this input to
CLKPROG.

Here's how it's done:

   !JOB DLITEON,MANAGER.SYS;OUTCLASS=,1
   !COMMENT   Switches to Daylight Savings Time.
   !IF NOT (HPDAY=1 AND HPMONTH=4 AND HPDATE>=1 AND HPDATE<=7) THEN
   !  TELLOP  DLITEON streamed on the wrong day!
   !  EOJ
   !ENDIF
   !
   !BUILD TIMEFILE;REC=-80,,F,ASCII;TEMP
   !FILE TIMEFILE,OLDTEMP;SHR;GMULTI
   !RUN FCOPY.PUB.SYS;INFO=":SHOWTIME";STDLIST=*TIMEFILE
   !
   !EDITOR
   TEXT TIMEFILE
   DELETE 1/3                  << just FCOPY headers, ignore them >>
   COPY 4 TO 5
   CHANGE "SUN, APR ", "04/", 4
   CHANGE ", 19", "/", 4
   CHANGE 9/72, "", 4          << get rid of time >>
   CHANGE 1/19, "", 5          << get rid of date >>
   CHANGE " AM", "", 5
   CHANGE " 2", " 3", 5        << increment hour >>
   :PURGE TIMEFILE,TEMP
   :FILE TIMEOUT=TIMEFILE;TEMP
   KEEP *TIMEOUT
   EXIT
   !
   !RUN CLKPROG.UTIL.SYS;STDIN=*TIMEFILE
   !EOJ

There  you  go.  Simple,  isn't it? (Heh heh  heh.) A very similar job
stream  will  switch back to non-Daylight  Savings Time -- just change
the  appropriate  months,  days,  and  hours.  I  imagine  that  minor
modifications  might also make this work for other countries that have
similar schemes.

Note that the above would be somewhat simpler in MPE/XL, and even more
simple in VESOFT's MPEX or STREAMX.

Incidentally,  you are quite correct that  some UNIX systems do indeed
have  this sort of thing built into  the operating system. In 1987, in
fact,  the vendors had to send out  a patch to their users because the
U.  S. Congress changed the changeover  dates to lengthen the Daylight
Savings  Time period. (The system could  predict the time changes, but
no computer can predict what Congress will do!)

Incidentally,  the U. S. Transportation  Department estimates that the
1987 changeover (which, I believe, lengthened Daylight Savings Time by
about  six or seven weeks) saved $28,000,000 in traffic accident costs
and avoided 1,500 injuries and 20 deaths. Believe it or not.


Q: The one thing that's always bothered me about the :REPLY command is
that  it  requires  this  completely  arbitrary PIN  value. My console
operator has to do a :RECALL, find the pin value, do the :REPLY -- why
can't  he just say :REPLY 7 and have the tape request be satisfied? Or
(to  avoid  possibly  :REPLYing  to  the  wrong request),  perhaps the
operator  could  somehow  identify the request  he's replying to, e.g.
:REPLY MYSTORE,7. This would be much simpler for my operators.

I  thought  that  HP  might  do  something  about  this with  its user
interface  improvements on the Spectrum, but  no dice -- I've tried it
on my MPE/XL machine and the :REPLY command is the same as it's always
been.

Is  there anything that can be done? I guess that somebody might write
a  program that goes through the right system tables, but I don't know
how to do this and in any case don't have the time.

A: Going through the system tables would be a pretty drastic approach.
After  all,  the  information you want (the  pin) is readily available
using  the :RECALL command -- the trick is capturing it and passing it
to the :REPLY command.

Well,  you might ask, how am I  going to "capture" the :RECALL output?
Unlike the :LISTF and :SHOWJOB commands, :RECALL does not have a "list
file"  parameter  -- all its output goes  to the terminal. How can the
output be sent to a file?

Fortunately,  the :RECALL output doesn't  actually go to the terminal;
it  goes  to  $STDLIST. If you can redirect  $STDLIST -- as you can by
running  a  program  with the ;STDLIST= parameter  -- then the :RECALL
output will go to the ;STDLIST= file. Thus, you can say:

   :PURGE MYLIST
   :BUILD MYLIST;REC=-80,,F,ASCII;NOCCTL
   :FILE MYLIST,OLD;SHR;MULTI
   :RUN FCOPY.PUB.SYS;INFO=":RECALL";STDLIST=*MYLIST

Why  do we run FCOPY here? We don't do it to copy any files -- rather,
we  do  it  because  FCOPY  can execute as an  MPE command any command
(prefixed by a ":") passed via ;INFO=. To use the ;STDLIST= parameter,
we  have to run a program; to get  the :RECALL output, we have to make
the  program  execute  the  :RECALL  command -- rather  than writing a
custom  program  that will do nothing but  a :RECALL, we might just as
well use FCOPY.

OK,  now  there's  a  file  called MYLIST that  has the :RECALL output
inside  it.  (Actually,  it  also  has  two  lines of  overhead at the
beginning.)  Now, we want to get the  PIN from the :RECALL listing and
do a :REPLY to that PIN -- how can we do it?

We  could  write  a  program  that  opens MYLIST, reads  it, finds the
correct  record,  and  issues  a  :REPLY  command  (using  the COMMAND
intrinsic). This would not be a trivial program (especially in COBOL);
furthermore,  the  moment  a user-written program  is introduced, that
substantially  increases the management overhead involved. You'll have
to  keep  track  of  both  the  source  file and the  program file (in
addition  to  the  command file or UDC that  runs the program); if you
need  to do the same thing on another machine, you'll have to transfer
at  least  two  files, possibly all three. I  prefer to have things as
self-contained and as simple as possible; whenever possible, I like to
avoid writing custom system programs.

But,  you ask, how is it possible to solve the problem without writing
a  third-generation-language  program?  Those of you  who are familiar
with  "MPE Programming" -- writing programs in the Command Interpreter
itself,  using  UDCs,  job streams, and (in  MPE/XL) command files and
variables  -- might already have a pretty good idea of how this can be
done.

By  the way, before we go any  further -- the solutions that I mention
below  are designed for MPE/XL. However, even if you're an MPE/V user,
they  may still be interesting to you, since you can learn a bit about
MPE/XL  from them. Furthermore, if  you use VESOFT's MPEX/3000, you'll
be  able  to  execute  these command files from  within it, since MPEX
emulates MPE/XL commands under MPE/V.

Our MYLIST file contains data that we want to read and use in a :REPLY
command.  It would be nice if there  were an :FREAD command that could
read  a  record  from  a file into an  MPE/XL variable; however, there
isn't.  There  is  an :INPUT command, but that  only asks the user for
input, so we can't use it, can we?

Of  course, we can. If we can redirect :RECALL's output to $STDLIST by
doing  the :RECALL through a program with ;STDLIST= redirected, we can
also  redirect :INPUT's input to MYLIST  by doing the :INPUT through a
program  with ;STDIN= redirected. In fact, if we only make MYLIST into
a  message  file, each :INPUT from the  file will delete the record it
read,  so we can (using a :WHILE loop) read the entire contents of the
file.

This is what our command file might look like:

   PARM FILENAME, REPLYVALUE
   PURGE MYLIST
   BUILD MYLIST;REC=,,F,ASCII;NOCCTL;MSG
   FILE MYLIST,OLD;SHR;GMULTI
   RUN FCOPY.PUB.SYS;INFO=":RECALL";STDLIST=*MYLIST
   WHILE FINFO('MYLIST',19)>0 DO
     RUN FCOPY.PUB.SYS;INFO=":INPUT MYLINE";STDIN=MYLIST;STDLIST=$NULL
     IF POS('"!FILENAME"',MYLINE)<>0 THEN
       SETVAR MYLINE RHT(MYLINE,LEN(MYLINE)-POS("/",MYLINE))
       SETVAR MYLINE RHT(MYLINE,LEN(MYLINE)-POS("/",MYLINE))
       REPLY ![LFT(MYLINE,POS("/",MYLINE)-1)],!REPLYVALUE
     ENDIF
   ENDWHILE

It  takes  two  parameters -- the name of  the tape file for which the
reply  was  issued  (in a message such as  'LDEV# FOR "MYTAPE" ON TAPE
(NUM)?',  this would be MYTAPE) and the  value to reply with (e.g. the
ldev). The way it works is as follows:

   * The  first  four  commands  --  :PURGE, :BUILD,  :FILE, and :RUN
     FCOPY;INFO=":RECALL"  -- send the :RECALL  into MYLIST, a message
     file.

   * The  "WHILE  FINFO('MYLIST',19)>0"  tells the CI  to execute the
     WHILE-loop  commands  while  the  number  of  records  in  MYLIST
     (returned  by  FINFO option 19) is greater  than 0, i.e. while we
     haven't read all the records.

   * The  :RUN  FCOPY;INFO=":INPUT MYLINE";STDIN=MYLIST  executes the
     :INPUT  command from the MYLIST file. Note that we run FCOPY with
     ;STDLIST=$NULL  so  that  the FCOPY headers  and such junk aren't
     printed.

   * The :IF makes sure that this  is indeed the right :REPLY request
     --  the one that refers to the  given filename; if it is, the two
     SETVARs  get  rid  of  the text of MYLINE  up to the second slash
     (this  will  be  the  time  and the job/session  number), and the
     ![LFT(MYLINE,POS("/",MYLINE)-1]  extracts  just  the PIN.  As I'm
     sure  you've guessed (if you didn't know it already), POS and RHT
     are  string-handling functions -- in  this case, we're using them
     to parse the :REPLY command output.

So,  there  we have it. It could use  a little bit of cleaning up; for
instance,  I'd  purge  the file MYLIST and  delete the variable MYLINE
after the command file is done; I'd check for the case where the reply
request we're looking for wasn't found and print an error message; I'd
also say

   SETVAR OLDHPMSGFENCE HPMSGFENCE
   SETVAR HPMSGFENCE 1
   PURGE MYLIST
   SETVAR HPMSGFENCE OLDHPMSGFENCE

instead  of  just  saying  "PURGE MYLIST" -- this  will avoid the ugly
warning  message  in  case  the  file  MYLIST doesn't  exist. ("SETVAR
HPMSGFENCE  1"  means  "print  error  messages  but  don't  print  any
warnings".)

Finally,  one problem with this solution  is that each :RUN FCOPY will
output  a few blank lines and an "END OF PROGRAM" message -- you might
want  to  :ECHO  some  escape  sequences  that  will  get rid  of this
undesirable output.

Had  enough? Are you happy? Confused?  Bored? Well, you're not off the
hook yet!

Take a look at the following command file:

   PARM FILENAME, REPLYVALUE
   ECHO ![CHR(27)+"J"]
   RECALL
   INPUT MYLINE;PROMPT=![CHR(27)+"A"+CHR(27)+"d"]
   WHILE MYLINE<>"THE FOLLOWING REPLIES ARE PENDING:" AND &
         MYLINE<>"NO REPLIES PENDING (CIWARN 3020)" AND &
         POS('"!FILENAME"',MYLINE)=0 DO
     INPUT MYLINE;PROMPT=![CHR(27)+"A"+CHR(27)+"A"+CHR(27)+"d"]
   ENDWHILE
   ECHO ![CHR(27)+"F"]
   IF MYLINE="THE FOLLOWING REPLIES ARE PENDING:" OR &
      MYLINE="NO REPLIES PENDING (CIWARN 3020)" THEN
     ECHO Error: No such reply request!
   ELSE
     SETVAR MYLINE RHT(MYLINE,LEN(MYLINE)-POS("/",MYLINE))
     SETVAR MYLINE RHT(MYLINE,LEN(MYLINE)-POS("/",MYLINE))
     REPLY ![LFT(MYLINE,POS("/",MYLINE)-1)],!REPLYVALUE
   ENDIF

What's going on here? Well, remember that an ASCII ESCAPE character is
character  number  27;  ![CHR(27)+"J"]  means "escape J",  which to HP
terminals  means  "clear the screen from the  cursor to the end of the
screen". After we clear the remainder of the screen, we

   * Do  a  :RECALL  --  note  that  this  :RECALL is  not redirected
     anywhere; it goes straight to your terminal.

   * Do an :INPUT  MYLINE;PROMPT=![CHR(27)+"A"+CHR(27)+"d"]
     The escape-"A" means move the cursor up one line; the escape-"d"
     means  TRANSMIT  THE  LINE ON WHICH THE  CURSOR IS LOCATED TO THE
     COMPUTER!  The escape-"d" tells the terminal to send the computer
     the  current line of text (nwhich is  now the bottom line of the
     :RECALL output) -- the :INPUT command dutifully picks up the line
     and puts it into MYLINE.

   * The :WHILE loop keeps doing these :INPUT commands until:

       -  MYLINE  is equal to "THE  FOLLOWING REPLIES ARE PENDING:" --
         this  would  mean  that  we've  read  through  all  the reply
         requests and haven't found the one we want;

       - MYLINE is equal to "NO REPLIES PENDING (CIWARN 3020)";

       -  or, MYLINE contains the filename we're looking for, in which
         case this is the reply request that we want.

   * When  we're done with the :WHILE  loop, we either print an error
     message  indicating that the reply request wasn't found, or parse
     the  reply request to get its pin (just as we did in the previous
     example).

Thus,  this second approach does exactly what the first approach does,
but with three advantages:

   * It doesn't require any :RUNs, so it can be done from break mode.

   * It  may  be somewhat faster (again,  because it doesn't :RUN any
     programs).

   * It doesn't output any pesky "END OF PROGRAM" messages.

As  you see, there's more than one  way to skin a :REPLY listing. Both
of them are good examples of how MPE/XL plus a little bit of ingenuity
can  solve  some  pretty  complicated problems. (The  ingenuity on the
second  example,  incidentally,  was  a  combination  of mine  and Gil
Milbauer's  -- one of our technical  support people at VESOFT. Thanks,
Gil.)


Q: I've  been  looking  at  the new  ACD  [Access Control Definition]
feature  in  V-delta-4  MIT  and I have one  question: where are these
things kept? Are they stored somewhere in the user labels? In the file
label?  (If they're in the file label,  where are they -- I've thought
that the file label was only 128 words long.)

A:  Naturally, this is the very same question that I asked myself when
we  first  got V-delta-4. There's almost no  room left in the 128-word
file label, certainly not enough to keep the ACD pairs (of which there
can be up to 20, each requiring at least 9 words of storage).

What  I did to figure this out was relatively straightforward. I built
a  file  without an ACD and did a  :LISTF ,-1 (show file label) of it;
then I added an ACD, did another :LISTF ,-1, and compared.

The  differences that I saw were in  words 120, 121, and 122. Word 122
was  a  "1"  (I'll  get  to it shortly), and  words 120 and 121 looked
suspiciously  like  a  two-word  disc  address  (just  like  the  disc
addresses  in the extent map -- the first 8 bits were the volume table
index and the last 24 bits were the sector address).

The  next step was to run DISKED5 and look at the sector pointed to by
the  label. Sure enough, there was the ACD in all its glory -- 6 words
of overhead information followed by 9 words for each pair (4 words for
the user name, 4 words for the account name, and 1 word for the access
mask).   In   a   way,   this  place  where  the  ACD  is  kept  is  a
"pseudo-extent",  just  like the file's 32 data  extents -- a chunk of
disc  space pointed to by the  file label. In fact, "pseudo-extent" is
exactly what HP itself calls it.

Of  course,  this  pseudo-extent  is  not  the  same size  as the data
extents;  it  is 1 or 2 sectors long,  depending on the ACD size. (The
number  of  sectors in the pseudo-extent is  kept in word 122.) If you
build  a  file  with  13  or  less pairs in its  ACD, only 1 sector is
allocated  (since  6+9*13  = 123 is less than  128); if you then add a
pair,  the  old  1-sector pseudo-extent will be  deallocated and a new
2-sector pseudo-extent will be allocated in its place.

Because  of  this,  I'm quite surprised that HP  limited the ACD to 20
pairs. Certainly there's room in even a 2-sector pseudo-extent for 27,
and  there's no reason that I can see why the pseudo-extent can't grow
to  3 or more sectors. The best guess  that I heard about this is that
somewhere  they  might  try to read the entire  ACD onto the stack and
they therefore don't want it to be too large. I'm not sure about this,
but  I  suspect  that  the 20-pair restriction  might prove bothersome
under some circumstances.

The  other  thing that you should keep in  mind with regard to ACDs is
that  --  as  of  press  time  -- both EDITOR and  TDP (and many other
editors)  throw  away ACD's of files they  /KEEP over. Since an ACD is
attached  to  the  file  label,  when the file  is purged and re-built
(which is what happens during a /KEEP), the ACD goes away. One can use
the HPACDPUT intrinsic to explicitly copy the ACD from the old file to
the new, but EDITOR and TDP don't do this.

I  had a lot of fun with this when I decided to change our "MPEX hook"
feature  to force EDITOR and TDP  (and many other editors) to preserve
these  ACDs,  even  though they were never  designed to! I patched the
programs'  calls  to  the  FCLOSE  intrinsic so that  when they did an
FCLOSE  with  disposition  4  (purge file) the ACD  of the purged file
would  be  saved  and when they then did  an FCLOSE with disposition 1
(save file) the saved ACD would be attached to the new file (if it had
the same name as the just-purged file).

Now  our hooked EDITOR, hooked TDP, hooked QUAD, etc. preserve ACDs of
files that you /KEEP over. Some time I might write something about how
these  "hooks"  are  done -- how programs can  be patched to do things
they  were never intended to do, like preserve ACDs, have a multi-line
REDO  command  history,  save files across  account boundaries (if you
have  SM), and so on. I find it  to be a very interesting and powerful
mechanism.


Q: I've been doing some stuff on the Spectrum in compatibility mode --
their  new  debugger  is great! It's so much  better and easier to use
than  the  Classic  Debugger  in  virtually  all  respects  -- setting
breakpoints  by  procedure name (rather  than octal address), symbolic
tracebacks, windows, etc.

Note  that I said VIRTUALLY all respects.  One thing that I can't seem
to  get  the  system  to  do is to drop me  into debug when my program
aborts  (using  the :SETDUMP command). If I do  a :SETDUMP, I do get a
nifty  symbolic  traceback  (which  tells  me exactly  where the abort
occurred),  but  I  can't  get  into  debug  to look  at my variables,
procedure  parameters, and so on -- all the stuff that can tell me why
the program aborted. I do a :HELP SETDUMP and it tells me that "if the
process is interactive, it will subsequently enter the system debugger
to  wait for further commands", but  that doesn't seem to happen. What
can I do?

A:  Well,  you're right. :HELP SETDUMP tells  you that a :SETDUMP will
get you into the debugger in case of a program abort; the MPE Commands
Manual  says  the  same  thing. But the  System Debug Reference Manual
says:

   "If  the  process  is  being  run  from  a session,  then after the
    specified  command  string  has been executed,  DEBUG will stop to
    accept  interactive  commands  with  I/O  performed  at  the  user
    terminal, CONTINGENT UPON THE FOLLOWING REQUIREMENTS:

       * The abort did not occur while in system code

       * The  process  entered  the  abort  code  via  a  native mode
         interrupt.

    NOTE: CM programs will usually fail these tests."

Because  of the way MPE/XL executes CM  programs, it seems that any CM
program  abort is always "in system code", and a program abort -- even
when  you've  done a :SETDUMP -- will  never (well, almost never) drop
you into the debugger.

Great  -- what now? Well, it turns out that there IS something you can
do  (though  only  if  you have PM capability).  Instead of having the
:SETDUMP  command  get you into debug AFTER  the abort occurs, you can
get into debug WHILE the aborting is in progress. Just say:

   :RUN MYPROG;DEBUG

   cmdebug > NM; B trap_handler,,,CM; E

This  sets  a  break  point at the  system SL procedure "trap_handler"
which  is called (to the best of my knowledge) whenever the program is
aborting.  When  "trap_handler"  is triggered, the  breakpoint will be
hit, and you'll be in DEBUG, able to look at anything you want.

Note   that   we   didn't   just   say   "B   trap_handler"   but   "B
trap_handler,,,CM".  The fourth parameter of the B command is the list
of  commands  to  be executed whenever the  breakpoint is hit; in this
case,  we  tell  DEBUG to do a "CM"  to get us into compatibility mode
DEBUG rather than into native mode DEBUG.

If  you do this, you don't need to do a :SETDUMP anymore. The bad part
is  that, unlike :SETDUMP, you can't do  it once for your session, but
rather  have  to do it every time you  run your program. If you really
want to, you can have your program call the HPDEBUG intrinsic with the
"NM;B  trap_handler,,,CM;E"  command  as  the  first  thing  it  does.
However,  since  HPDEBUG  is a Native Mode  intrinsic, your CM program
would  have  to  call it via Switch  (with the HPSWTONMNAME intrinsic)
rather than call it directly.

One  cautionary  note:  It's  always risky to  rely on system internal
procedures  like  "trap_handler",  since they can  change at any time.
I've  tested  this  on  MPE/XL Version A.10.01  (colloquially known as
"MPE/XL 1.1") -- I make no promises beyond that.


Q:  One of my programs has a bug that only seems to manifest itself in
a  particular  (very long) job stream. Whenever  I try to duplicate it
online,  I  can't; however, if it's running  in batch, I can't use the
debugger.  Finally  I  manage to learn how  to use DEBUG (fortunately,
it's  a lot more powerful on MPE/XL  than on MPE/V), and I can't apply
to it to the one problem I need to fix!

Is there anything I can do?

A:  Yes, there is. Fortunately,  among the many wonderful improvements
implemented  in DEBUG/XL is the ability to debug batch programs on the
console. If you go into DEBUG and say

   ENV JOB_DEBUG = TRUE

then  whenever a job calls DEBUG (usually as a result of a :RUN ;DEBUG
or  of a Native Mode program  abort following a :SETDUMP command), the
debugger  will start executing on the  system console on behalf of the
job.  You  can  do  anything  in  this  mode  that you  normally would
(remembering  that you're actually debugging  the job, not whatever is
running  on the console at the time); you can type "EXIT" to leave the
debugger and resume the job.

But,  you  ask, what about the session  that I already have running on
the  console?  Excellent question! The session  keeps running -- keeps
sending  whatever output it has to the console (where it will come out
mixed  in with the debugger output) -- even worse, keeps prompting you
for input on the console!

So  what can you do? You must  somehow suspend your console session so
that  it  will no longer try to  use the terminal while the debugger's
using it. My recommendation is to say

   BUILD MSGFILE;MSG
   RUN FCOPY.PUB.SYS;STDIN=MSGFILE;STDLIST=$NULL

as  soon as the DEBUG header appears  on the console. You must be sure
that  you  enter  this  in  response to your  console session's prompt
instead  of in response to the  "nmdebug> " prompt. (These two prompts
will  more or less alternate until you actually manage two execute the
BUILD and the RUN.) Once you manage to get these two commands in, your
console  session will be waiting on the message file wait, and you can
go ahead using the debugger.

When  you  exit  the  debugger,  you'll  be back to  your hung console
session. To unhang it, just hit [BREAK] and say

   FILE MSGFILE,OLD;DEL
   LISTF MSGFILE;*MSGFILE
   RESUME

The  :LISTF command will write a  few records into MSGFILE, which will
(when  the  :RESUME  is  done)  wake  up  FCOPY and  return control to
wherever your console session was before.

So,  there you go. A little bit of  work, but it's worth it -- you can
now debug your program in exactly the environment in which it fails.

A couple of other things worth mentioning:

   * If  your  program  is aborting with  a BOUNDS VIOLATION, INTEGER
     OVERFLOW,  or some such error, you  should probably do a !SETDUMP
     command  in  your  job before !RUNing your  program -- this makes
     sure that DEBUG will be called when the program aborts.

     This  will  work quite well for  MPE/XL Native Mode programs, but
     not  for Compatibility Mode programs; even  if you do a !SETDUMP,
     CM  programs will usually NOT drop you into the debugger at abort
     time   (although  you  can  still  !RUN  them  with  ;DEBUG,  set
     breakpoints, etc.).

     However, if you say in your job stream

        !RUN CMPROG;DEBUG

     and then enter at the console (when the debugger will be invoked)

        B trap_handler,,,CM

     then  any program abort WILL call DEBUG  (whether or not you do a
     !SETDUMP)  --  this  is  because  "trap_handler"  is  the  system
     procedure  that's called at program abort  time, and by setting a
     breakpoint  at  it,  you  make  sure  that  DEBUG will  be called
     whenever "trap_handler" is called.

   * If  you  execute MPE commands  from within DEBUG-on-the-console-
     running-on-behalf-of-your-job   (using   the  :  command),  these
     commands  will also be executed on behalf of your job (not of the
     console  session). HOWEVER, this means  that their output will be
     sent  to  your job's $STDLIST, where you  won't be able to see it
     until the job finishes!

     Thus,  if you want to do a  :LISTEQ, a :LISTFTEMP, a :SHOWVAR, or
     whatever  to  see what's going on in  the job, you're in trouble.
     Your  best  bet  in  this  case  would  probably  be to  send the
     command's  output to a disc file and  then look at this file from
     another terminal.

     On  the  other  hand,  remember that you can  use this feature to
     :BUILD or :PURGE job temporary files, set :FILE equations, etc.


Q:  From   within  our  COBOL  programs   we   sometimes   get   these
nasty-looking   "Illegal  ASCII  digit"  messages;  the  program  then
continues, but the results are usually meaningless.

Question  #1: How can we tune the system to abort  the  program  after
printing the usual "Illegal ASCII digit" stuff?

Question  #2:   As we have bought third-party programs (e.g. AR/AP) it
might  happen that those  programs  abort  now,  which  would  not  be
desirable.    Is  it  possible  to  move  the  modified trap procedure
(COBOLTRAP?)  to a group SL so that we can run the programs  which  we
want to abort with ;LIB=G or ;LIB=P?

Question  #3:   Is it possible to customize the procedure depending on
whether  the error occurs in a session  or  in  a  job?   All  of  our
interactive   programs  use  VPLUS, so it is not very amusing to watch
the  error message running through the fields.   Most  preferably  the
program   should  get  back  an  error  along  with  the  source-  and
programcounter-  addresses.  Also, we'd like to see block mode  turned
off and then have the usual message displayed with the final abort.

A: Good questions.  Let's see if I can give you some good answers.

First  of  all, your questions seem to imply that you're looking for a
global   sort  of  modification,  perhaps  a  patch   to   SL.PUB.SYS.
Actually,   this  is  possible,  but  I would not advise it.  I try to
avoid  operating system patches as much as possible --  they  give  HP
understandable  concerns about supporting your systems, they're not at
all   trivial  to  install,  and  they may have to be re-developed for
every  new version of the operation system.  Although several  vendors
have   had  very good experience with products that patch MPE, I would
recommend  that ordinary users not do it themselves (the vendors  have
the  time and money needed to support this kind of thing -- most users
don't).

Fortunately,  there's a non-OS-patch solution that should take care of
(at   least)  your  questions  #1  and  #2.   Actually,  there are two
solutions  -- a simple one if you're using  an  MPE/XL  system  and  a
somewhat more complicated one if you're using MPE/V.

On  MPE/XL, the COBOL II/XL compiler DOES NOT (by default)  CHECK  FOR
ILLEGAL   ASCII/DECIMAL  DIGITS!  In other words, by default, any such
errors  will be silently  ignored.   If,  however,  you  compile  your
program    with   the   $CONTROL  VALIDATE  compiler  option,  illegal
ASCII/decimal  digits will cause the program  to  ABORT  (rather  than
print a warning, do a fix-up, and continue).

You  can,   however (if you use the right $CONTROL options), configure
the  exact behavior in case of this error  using  an  MPE/XL  variable
called   COBRUNTIME.   It  is  supposed to be a string of six letters;
each  letter indicates what is to be done in case of one of six  kinds
of error, the possible values for each letter being:

   A = abort
   I = ignore
   C = comment (message only)
   D = DEBUG (go into DEBUG in case of error)
   M = message and fixup
   N = no message but fixup

The letter assignments are:

   letter 1 governs behavior in case of ILLEGAL DECIMAL OR ASCII DIGIT;
   letter 2 pertains to RANGE ERRORS;
   letter 3 pertains to DIVIDES BY ZERO OR EXPONENT OVERFLOWS;
   letter 4 pertains to INVALID GOTOS;
   letter 5 pertains to ILLEGAL ADDRESS ALIGNMENT;
   letter 6 pertains to PARAGRAPH STACK OVERFLOW.

Letter  1 is in effect only if you use $CONTROL  VALIDATE;  letters  2
and   6  are in effect only if you use $CONTROL BOUNDS; letter 5 is in
effect   only  if  you  use  $CONTROL  BOUNDS  and   either   $CONTROL
LINKALIGNED or $CONTROL LINKALIGNED16.

Thus, if you say

   :SETVAR COBRUNTIME "AIDDDD"

this  tells the COBOL II/XL run-time  library  to  abort  in  case  of
illegal   decimal  or  ASCII  digit,  ignore range errors, and go into
DEBUG  for any of the other four conditions.  (You might want  to  put
this   :SETVAR  into  some  OPTION  LOGON  UDC so that it is always in
effect).

The solution for Classic (non-Spectrum) machines is not as easy.

How  does COBOL detect, report, and try to fix up illegal ASCII digits
and  similar errors?  Well, the HP3000's  normal tendency in  case  it
finds   this  kind  of error would be to abort the program (just as it
normally  would for a stack overflow, a bounds violation, a divide  by
zero,   etc.).  However, COBOL decided to do things differently, so it
took advantage of the XARITRAP intrinsic.

XARITRAP  lets you indicate that when a  particular  arithmetic  error
occurs,   the  program  should not be aborted but instead a particular
"trap  procedure" should be called.  XARITRAP also  lets  you  specify
exactly   which  arithmetic traps are to be re-routed through the trap
procedure;  that way, your program might, for instance,  trap  integer
overflows but not divides by zero.

The  first thing that a COBOL program does when it's run (well, almost
the   first  thing)  is  call the system SL COBOLTRAP procedure.  This
COBOLTRAP  procedure calls the XARITRAP intrinsic, telling it that all
integer  and decimal arithmetic errors should  be  passed  to  another
system  SL procedure called C'TRAP.  C'TRAP is the one that prints the
error   message,  tries the fix-up, and also handles other errors that
you  don't even see (for instance, integer overflows  and  divides  by
zero, which it silently ignores).

The  trick   is  that  when  COBOLTRAP  calls XARITRAP, it passes it a
"mask"   value  of  octal  %23422;  if  you  look  at   the   XARITRAP
documentation   in  the Intrinsics Manual, you'll find that this traps
Integer  Divide  By  Zero,  Integer  Overflow,  and  all  the  decimal
arithmetic errors (including your Illegal ASCII Digit error).

Now,  as I said before, from MPE's point of view it's only natural for
any   one  of  these arithmetic errors to cause your program to abort.
COBOLTRAP  changes this default assumption; all you need to do  is  to
revert  to the original state by unsetting the trap.  One way of doing
this would simply be:

   77 DUMMY         PIC S9(4) COMP.
   ...
   << near the start of your program, do the following: >>
   CALL "XARITRAP" USING 0, 0, DUMMY, DUMMY.

This  simply turns off ALL arithmetic traps -- whenever any decimal or
integer arithmetic error occurs, the program is aborted.

The  problem with this is that this  disables  ALL  arithmetic  traps,
including   integer  divides by zero and integer overflows.  This is a
good  deal broader than your original request of causing  aborts  only
on  Illegal ASCII Digits; it's possible that your programs rely on the
trapping   of  integer  overflows,  integer divides by zero, and other
conditions.   (I've even heard rumors that the code generated  by  the
COBOL   compiler ALWAYS relies on integer overflows being trapped, but
I can't vouch for the truth of this.)

So  what can you do?  Well, the next step would  be  to  do  something
like this:

   77 DUMMY         PIC S9(4) COMP.
   77 OLD-MASK      PIC S9(4) COMP.
   77 OLD-PLABEL    PIC S9(4) COMP.
   ...
   << near the start of your program, do the following: >>
   CALL INTRINSIC "XARITRAP" USING 0, 0, OLD-MASK, OLD-PLABEL.
   COMPUTE OLD-MASK = OLD-MASK - (OLD-MASK / 256) * 256.
   CALL INTRINSIC "XARITRAP" USING OLD-MASK, OLD-PLABEL, DUMMY, DUMMY.

What's going on here?

   * We first call XARITRAP to disable all the  traps;  however,  the
     real   reason  we do this is to get back the OLD mask and the OLD
     trap procedure address (PLABEL).

   * Then, we use the COMPUTE statement to eliminate the high-order 8
     bits  of the mask -- these refer to  all  the  decimal  traps;  I
     presume that you'd like to see ALL of these cause aborts.

   * Finally,   we  call XARITRAP again, passing the new mask and the
     old  plabel (to re-enable the  trap  procedure  that  we've  just
     disabled).

This  way, you make the decimal traps cause aborts while retaining the
normal behavior for integer overflows and integer divides by zero.

Does  this   make  sense?   Did you put those lines into your program?
Are  you happy?  Well, you shouldn't be!  Unfortunately,  despite  all
my   reasonable  arguments,  the  above  code will do exactly what the
previous  example did -- it will disable  ALL  arithmetic  traps,  not
just the decimal ones!  Why?

Well,  the   last  wrinkle  that  you  have to iron out is caused by a
little  check that the  XARITRAP  procedure  does.   If  you  look  up
XARITRAP in your Intrinsics Manual, you'll see the following lines:

   "If,  when, a trap procedure is being  enabled,  the  code  of  the
    caller   is ... nonprivileged in PROG, GSL, or PSL, plabel must be
    nonprivileged in PROG, GSL, or PSL."

Makes  sense   to  you?  Of course it does -- clear as day!  Actually,
what  this means is that when you enable  a  trap  from  your  program
(which   you're doing with the second XARITRAP), you can't tell MPE to
trap  to a procedure in the system SL (which is what we're telling  it
to   do by passing the plabel we got back from the first XARITRAP call
-- the plabel of the system SL C'TRAP procedure).

I  think that this is actually a security feature (otherwise you might
be  able to trap to some internal MPE procedure which might then crash
the  system and generally cause unpleasantness); however,  this  means
that  those three lines -- the first CALL "XARITRAP", the COMPUTE, and
the  CALL "XARITRAP" actually have to go into a procedure that is kept
in the system SL.  Such a procedure might be:

   $CONTROL DYNAMIC, NOSOURCE, USLINIT
    IDENTIFICATION DIVISION.
    PROGRAM-ID. VENODECTRAP.
    AUTHOR. Eugene Volokh of VESOFT, Inc.
    DATE-WRITTEN. 6 February 1988.
    ENVIRONMENT DIVISION.
    CONFIGURATION SECTION.
    DATA DIVISION.
    WORKING-STORAGE SECTION.
    77  MASK                      PIC S9(4) COMP.
    77  PLABEL                    PIC S9(4) COMP.
    PROCEDURE DIVISION.
    COBOLERROR-PARA.
        CALL INTRINSIC "XARITRAP" USING 0, 0, MASK, PLABEL.
        COMPUTE MASK = MASK - (MASK / 256) * 256.
        CALL INTRINSIC "XARITRAP" USING MASK, PLABEL, MASK, PLABEL.
        GOBACK.

Now, to turn off decimal traps, your program should just say:

   CALL "VENODECTRAP".

Since  VENODECTRAP will be in the system SL, the second XARITRAP  call
(which re-enables the system SL C'TRAP procedure) will work.

A  few more points remain.  For one, what if you ONLY want to abort in
case   of  an  Illegal  ASCII Digit error and don't want to change the
behavior  in case of the other errors?  What you need to do  for  that
is   turn  off  the  bit  in the MASK variable that corresponds to the
Illegal  ASCII Digit error (which is  bit  6);  you  can  do  this  by
saying:

   COMPUTE MASK = MASK - 512.

instead  of  the original "COMPUTE MASK = ..." statement.  This should
always  work as long as  the  bit  is  actually  set  to  begin  with;
otherwise,   the  subtraction  of 512 will have rather unusual results
(it'll  probably ENABLE trapping of Illegal  ASCII  Digit  errors  but
disable  the trapping of Illegal Decimal Digit errors).  This could be
a problem if your program calls VENODECTRAP twice.

Alternatively, you could instead say

   MOVE %22422 TO MASK.

Normally,  as   I  mentioned,  the  COBOL  library sets the mask to be
%23422;  %22422 is the same but with  bit  6  turned  off.   The  only
trouble   with  this  approach  is  that  if the COBOL library is ever
changed  to trap some other things,  the  %22422  could  inadvertently
unset the traps that the new library procedures set.

Actually,  we discussed doing bit arithmetic in COBOL a few months ago
in   this column; if you want to, you can find this discussion and use
the  algorithms listed there to unset bit 6.  It  was  a  long  story,
however, and I don't want to get further into it just now.

Finally,  your   question  #3  was:  how  do  I avoid having the abort
displays  appear in my V/3000 forms  fields?   This  is  a  much  more
difficult   question.   It  would  be  nice  if one could set one trap
procedure  (the default C'TRAP) for the traps that you want  COBOL  to
handle   normally  and  a different trap procedure for those for which
you  want to abort; then, your own trap procedure  could  get  out  of
block mode before aborting.

Unfortunately,  MPE doesn't let you have two different arithmetic trap
routines   in  effect  at  once (even if they apply to different error
conditions).   One thing that I can  recommend  is  a  package  called
ATLAS/3000 from

   STR Software Co. P. O. Box 12506 Arlington, VA 22209 (703) 689-2525
   fax (703) 689-4632

ATLAS/3000  traps all sorts of errors (COBOL errors, IMAGE errors, job
errors,  file system errors, etc.), logs them,  reports  them  to  the
console,   and  so  on.   I'm  not  sure that it's exactly what you're
asking for, but it could quite possibly help you out in this area.

One  last point: by disabling the normal COBOL  trapping  for  Illegal
ASCII   digits,  you're  also  losing  the  SOURCE  ADDRESS and SOURCE
displays  that the  trap  routine  normally  gives  you;  these  could
sometimes   help  you  identify  which piece of data is bad.  You also
lose  the STACK DISPLAY that the cobol library prints for  you  (which
shows   you  not only where in your code the abort occurred but also a
traceback  of which procedures called that piece  of  code);  however,
you can re-enable this stack display by saying

   :SETDUMP

before running your program.


Q:  I have what you might think to be a strange question --  where  is
the Command Interpreter?

On  my   MPE/XL  machine, I see a program file called CI.PUB.SYS, so I
guess  that that's the  Command  Interpreter  program  (the  one  that
implements   :RUN,  :PURGE,  etc.)  -- however, I couldn't find such a
program on my MPE/V computer.

:EDITOR,  I know, is implemented  through  EDITOR.PUB.SYS;  :FCOPY  is
FCOPY.PUB.SYS;  all the compilers have their own program files.  Where
is the CI kept?

A:  This  is a very interesting question -- one that bothered me for a
while several years ago.  The answer is somewhat surprising.

Virtually  all of the operating system on MPE/V is kept in the  system
SL   (SL.PUB.SYS).  This includes the file system, the memory manager,
etc.;  it also includes the Command Interpreter.  But,  you  may  say,
each   job  or  session  has  a  Command Interpreter process, and each
process  must have a program file attached to it.  Nope.  Each of YOUR
processes  must have a program file attached to it; MPE  itself  faces
no such restrictions.

All  that   a  process really has to have is a data segment (which MPE
builds  for each CI process when  you  sign  on)  and  code  segments;
internally,  there's no reason why those code segments can't be in the
system   SL.   MPE  just uses an internal CREATEPROCESS-like procedure
that  creates a process given not a program file name but  an  address
(plabel) of a system SL procedure; no program file needed.

On  MPE/XL,   the situation is somewhat different (as you've noticed);
there's  a program file called CI.PUB.SYS that is actually what is run
for  each CI process.  However,  if  you  do  a  :LISTF  CI.PUB.SYS,2,
you'll   find that it's actually quite small (186 records on my MPE/XL
1.1  system); since Native Mode program files usually take a lot  more
records  than MPE/V program files, those 186 records can't really do a
lot   of  work.   CI.PUB.SYS  is  really just a shell that outputs the
prompt,  reads the input, and executes it  by  calling  MPE/XL  system
library   procedures.   MPE/XL  library procedures (including the ones
that  CI.PUB.SYS calls, either directly, or indirectly)  are  kept  in
the files NL.PUB.SYS, XL.PUB.SYS, and SL.PUB.SYS.

Thus,  in   both  MPE/V and MPE/XL, virtually all of the smarts behind
command  execution is kept  in  the  system  library  (or  libraries).
Notable   exceptions include all the "process-handling" commands, like
EDITOR,  FCOPY, STORE/RESTORE (which use the  program  STORE.PUB.SYS),
and    even  PREP  (which  actually  goes  through  a  program  called
SEGPROC.PUB.SYS).   However, :FILE, :PURGE, :BUILD, and so on, are all
handled by SL, XL, or NL code.


Q:  A couple of months ago I read in this column that you can speed up
access  to databases by having somebody  else  open  them  first.   Is
there   some easy way that I can take advantage of this without having
to  write a special program?  I guess what I'd have to do  is  have  a
job   that  DBOPENs  the  database and then suspends with the database
open, but how can this be easily done?

A:  Indeed, if a TurboIMAGE database is already DBOPENed  by  somebody
then   a  subsequent DBOPEN will be faster than it would be if it were
the  first DBOPEN.  On my Micro XE, a DBOPEN of a database that nobody
else  has open took about 2 seconds; a DBOPEN of a database  that  was
already   opened  by  somebody  else took only about 0.8 seconds.  The
first  DBGET/DBPUT/DBUPDATE against a dataset were also faster if  the
database   was  already open (for the DBGET, about 50 milliseconds vs.
about 400-500 milliseconds).

How  can you take  advantage  of  this?   Well,  if  the  database  is
constantly  in use (e.g. it's opened by each of the many users that is
accessing   your  application  system),  then  the  time savings comes
automatically  -- the first user will spend 2 seconds  on  the  DBOPEN
and   all  subsequent  users  will  spend  0.8 seconds, as long as the
database is open by at least one other user.

The  problem arises when your database is  frequently  opened  but  is
left   open for only a short time; for instance, if it is used by some
short  program that only needs  to  do  a  few  database  transactions
before  it terminates.  Then, you might have hundreds of DBOPENs every
day,   the  great  majority  of  which happen when the database is NOT
already  open.  You'd like to have one job hanging out  there  keeping
the   database  open  so  that  all  subsequent  DBOPENs will find the
database already opened by that job.

How  can this be done without writing a special  custom  program?   (I
agree   that  you should try to avoid writing custom programs whenever
possible  -- if you wrote a special program, you'd have to keep  track
of   three  files [the job stream, the program, and the source] rather
than just the job stream.) Well, let's take this one step at a time.

Having a job stream that opens your database is easy -- just say

   !JOB OPENBASE,...
   !RUN QUERY.PUB.SYS
   BASE=MYBASE
   <<password>>
   1   <<the mode>>

Unfortunately,  as soon as this job opens the database, it'll start to
execute  all the subsequent QUERY commands; eventually (because of  an
>EXIT   or  an end-of-file) QUERY will terminate and the database will
get  closed.  It would be nice  if  QUERY  had  some  sort  of  >PAUSE
command   (or,  to  be  precise,  a  >PAUSE  FOREVER  command), but it
doesn't.

Or  does  it?   What  are  the  mechanisms  that  MPE  gives  you  for
suspending a process?  Can we use any of them from within QUERY?

Well,  there's obviously the PAUSE intrinsic, which pauses for a given
amount   of time.  It's not quite what we want (since we want to pause
indefinitely),  but more importantly there is really no way of calling
PAUSE  from within QUERY (unless your QUERY is  hooked  with  VESOFT's
MPEX).   So much for that idea.  There are also a few other intrinsics
--   for instance, SUSPEND and PRINTOPREPLY -- that suspend a process,
but   they're  not  callable  from  QUERY  either.   You  can't   call
intrinsics   from QUERY; you can't even do MPE commands (though in any
event there aren't any MPE commands that suspend a process).

One  other thing, however, comes to mind --  message  files.   A  read
against   an  empty  message  file is a good way to suspend a process;
however,  QUERY doesn't have  an  ">FOPEN"  or  an  ">FREAD"  command,
either, does it?

Well,  in  a way it does.  QUERY's >XEQ command lets you execute QUERY
commands  from an external MPE file, and to do that it has to FOPEN it
and FREAD from it.  You might therefore say:

   !JOB OPENBASE,...
   !BUILD TEMPMSG;MSG;TEMP;REC=,,,ASCII
   !RUN QUERY.PUB.SYS
   BASE=MYBASE
   <<password>>
   1   <<the mode>>
   XEQ TEMPMSG
   EXIT
   !EOJ

The  XEQ will start reading from this temporary message file, see that
it's  empty, and hang, waiting for a record  to  be  written  to  this
file.    Since  the file is a temporary file, nobody else will be able
to write to it, so the job will remain suspended until it's aborted.

There  are a few other alternatives along the same  lines.   For  one,
you   could  make the message file permanent -- that way, you can stop
the  job by just writing a record to this file.  Why would you want do
this?   Because you may want to have  your  backup  job  automatically
abort the OPENBASE job so that the database can get backed up.

If  the   message file were permanent, your backup job could then just
say:

   !FCOPY FROM;TO=OPENMSG.JOB.SYS
   << just a blank line -- any text will do >>
   ...

The  one thing that you'd have to watch out for is the security on the
OPENMSG.JOB.SYS  file -- you wouldn't want to  have  just  anybody  be
able   to  write to this file because any commands that are written to
the  OPENMSG file will be executed by the QUERY in  the  OPENBASE  job
stream.  (Remember the >XEQ command.)

If  you   don't use the permanent message file approach, you can still
have the background job abort the OPENBASE job by saying

   !ABORTJOB LOCKBASE,user.account

However,  for this, you'd either have to  have  :JOBSECURITY  LOW  and
have  your backup job log on with SM capability, or have the :ABORTJOB
command   be globally allowed (unless you have the contributed ALLOWME
program or SECURITY/3000's $ALLOW feature).

Another  alternative is to >XEQ a file that requires a  :REPLY  rather
than a message file, e.g.

   !JOB OPENBASE,...
   !FILE NOREPLY;DEV=TAPE
   !RUN QUERY.PUB.SYS
   BASE=MYBASE
   <<password>>
   1   <<the mode>>
   XEQ *NOREPLY
   EXIT
   !EOJ

The  trouble with this solution is that  the  console  operator  might
then   accidentally  :REPLY  to the "NOREPLY" request.  (Also, the job
wouldn't quite work if you had an auto-:REPLY tape drive!)

In  any event, though, one of the above solutions might be  the  thing
for  you.  It's not going to buy you too much time (it only saves time
for   DBOPENs  and the first DBGETs/DBPUTs/DBUPDATEs on each dataset),
it  isn't necessary if the database is already likely to be opened all
the  time, and it won't work  on  pre-TurboIMAGE  or  MPE/XL  systems.
However,   in  some  cases  it  could  be  a  non-trivial (and cheap!)
performance improvement.

One  note to keep in mind (if you really look  carefully  at  the  way
IMAGE   behaves):   the  BASE=  in  the  job stream will only open the
database  root file -- the individual  datasets  will  remain  closed.
HOWEVER,   once the keep-the-database-open job starts, any open of any
dataset  by any user will leave the dataset open for all  the  others;
even   when  the  user  closes  the  database, the datasets will still
remain  open.  If the user only reads the dataset (and  doesn't  write
to   it),  the  dataset  will only be opened for read access; however,
once  any user tries to write to the  dataset,  the  dataset  will  be
re-opened for both read and write access.

The  upshot of this is that the database will start out with only  the
root   file opened, and then (as users start accessing datasets in it)
will  slowly have more and more of its datasets  opened.   After  each
dataset   has  been accessed (especially written to) at least once, no
more  opens will be necessary until the  last  user  of  the  database
(most likely the keep-open job itself) closes it.


Q:  I recently tried to develop a  way to periodically check my system
disk  usage  (free,  used,  and lost). I based  it on information that
VESOFT once supplied in one of their articles.

My procedure was as follows:

   1.  Do  a  :REPORT  XXXXXXXX.@ into a file  to determine total disk
      usage by account.

   2.  Subtotal the list (in my case, using QUIZ) to get a system disk
      space usage total.

   3. Run FREE5.PUB.SYS with ;STDLIST= redirected to a disk file.

   4.  Use QUIZ to combine the two output files and convert the totals
      to Megabytes.

This  way, I can show the total free space and total used space on one
report for easy examination. I can also add these two numbers, compare
the sum to the total disk capacity, and thus determine the 'lost' disk
space.

My  question is in regard to the lost disk space. The first time I ran
the  job,  the  total lost disk space came  out to be approximately 19
Megabytes. After doing a Coolstart with 'Recover Lost disk Space', the
job  again showed a total lost disk  space of 19 Megabytes. Didn't the
Recover  Lost disk Space save any  space? Could there be something I'm
overlooking?

A: Unfortunately, there is. Paradoxical as it may seem, the sum of the
total  used  space and the total free space  is NOT supposed to be the
same  as the total disk space on  the system; or, to be precise, there
is more "used space" on your system than just what the :REPORT command
shows you.

The  :REPORT  command shows you the  total space occupied by PERMANENT
disk FILES. However, other objects on the system also use disk space:

   - TEMPORARY FILES created by various jobs and sessions;

   - "NEW" FILES, i.e. disk files that are opened by various processes
     but not saved as either permanent or temporary;

   -  SPOOL  FILES,  both  output  and  input  (input spool  files are
     typically the $STDINs of jobs);

   - THE SYSTEM DIRECTORY;

   - VIRTUAL MEMORY;

   -  and various other objects, mostly  very small ones (such as disk
     labels, defective tracks, etc.).

Your job stream, for instance, certainly has a $STDLIST spool file and
a  $STDIN file (both of which use disk space), and might use temporary
files  (for instance, for the output  of the :REPORT and FREE5); also,
if  you weren't the only user on the  system, any of the other jobs or
sessions might have had temporary files, new files, or spool files.

In  other words, to get really precise "lost space" results, you ought
to:

   * Change  your  job  stream to take into  account input and output
     spool  file  space  (available  from  the  :SHOWIN  JOB=@;SP  and
     :SHOWOUT  JOB=@;SP  commands). Since :SHOWIN  and :SHOWOUT output
     can  not  normally  be  redirected  to  a  disk file,  you should
     accomplish  this  by  running  a  program  (say, FCOPY)  with its
     ;STDLIST=  redirected and then making  the program do the :SHOWIN
     and :SHOWOUT, e.g. by saying

       :RUN FCOPY.PUB.SYS;INFO=":SHOWOUT JOB=@;SP";STDLIST=...

   * Consider  the space used by the  system directory and by virtual
     memory (these values are available using a :SYSDUMP $NULL).


   * Consider the space used by your own job's temporary files.


   * Run the job when you're the only user on the system.

Your best bet would probably be to run the job once, immediately after
a  recover lost disk space, with nobody  else on the system; the total
'unaccounted  for' disk space it shows you (i.e. the total space minus
the :REPORT sum minus the free disk space) will be, by definition, the
amount  of  space  need by the system and  by your job stream. You can
call    this   post-recover-lost-disk-space   value   the   'baseline'
unaccounted-for total.

If  your job stream ever shows you  an 'unaccounted for' total that is
greater  than  the  baseline, you'll know that  there MAY be some disk
space lost. To be sure, you should

   * Run the job when you're the only user on the system.

   * Make sure that your session (the  only one on the system) has no
     temporary files or open new files while the job is running.

If  you  do all this, then comparing  the 'unaccounted for' disk space
total against the baseline will tell you just how much space is really
lost.

Incidentally, note that you needn't rely on your guess as to the total
size  of your disks -- even this  can be found out exactly (though not
easily).  The  very end of the  output of VINIT's ">PFSPACE ldev;ADDR"
command  shows  the  total  disk size as  the "TOTAL VOLUME CAPACITY".
Thus,  you could, in your job,  :RUN PVINIT.PUB.SYS (the VINIT program
file)  with ;STDLIST= redirected to a  disk file and issue a ">PFSPACE
ldev;ADDR"  command for each of your disk  drives. (If you want to get
REALLY  general-purpose,  you  can  even  avoid  relying on  the exact
logical device numbers of your disks by doing a :DSTAT ALL into a disk
file  and  then  converting  the output of this  command into input to
VINIT).

Finally,  it's  important  to  remember that all  this applies ONLY to
MPE/V.  The rules of the game for  MPE/XL are very, very different; in
any event, disk space on MPE/XL should (supposedly) never get lost.


Q:  I  want  to "edit" a spool file --  make a few modifications to it
before  printing  it. SPOOK, of course, doesn't  let you do this, so I
decided  just to use SPOOK's >COPY command  to copy the file to a disk
file,  use  EDITOR to edit it, and  then print the disk file. However,
when  I  printed  the  file,  the  output  pagination  ended up  a lot
different  from  the  way it was in the  original spool file! Is there
some  special  way  in  which  I should print the  file, or am I doing
something else wrong?

A:  Well, first of all, there is a special way in which you must print
any  file that used to have Carriage Control before you edited it with
EDITOR.  When  EDITOR  /TEXTs in a CCTL  file, it conveniently forgets
that  the file had CCTL -- when it /KEEPs the file back, the file ends
up  being a non-CCTL file (although  the carriage control codes remain
as data in the first column of the file). To output the file as a CCTL
file, you should say

   :FILE LP;DEV=LP
   :FCOPY FROM=MYFILE;TO=*LP;CCTL

The  ";CCTL" parameter tells FCOPY to interpret the first character of
each record as a carriage control code.

Actually,  you  probably already know this,  since otherwise you would
have  asked  a  slightly  different  question. You've  done the :FCOPY
;CCTL,  and  you have still ended  up with pagination that's different
from what you had to begin with. Why?

Unfortunately,  not all the carriage  control information from a spool
file gets copied to a disc file with the >COPY command. In particular:

   * If your program sends carriage control codes to the printer using
     FCONTROL-mode-1s instead of FWRITEs, these carriage control codes
     will be lost witha >COPY.

   * If  the  spool  file you're copying is  a job $STDLIST file, the
     "FOPEN"  records  (which  usually  cause form-feeds  every time a
     program is run) will be lost.

   * And,  most  importantly,  if  your  program  using "PRE-SPACING"
     carriage control rather than the default "POST-SPACING" mode, the
     >COPYd spool file will not reflect this.

Statistically  speaking,  it  is  this third point  that usually bites
people.

The  MPE  default  is  that when you write  a record with a particular
carriage  control  code,  the  data will be output  first and then the
carriage   control  (form-feed,  line-feed,  no-skip,  etc.)  will  be
performed  --  this  is  called POST-SPACING.  However, some languages
(such  as  COBOL or FORTRAN) tell the  system to switch to PRE-SPACING
(i.e. to do the carriage control operation before outputting the data),
and  it is precisely this command -- the switch-to-pre-spacing command
-- that is getting lost with a >COPY.

Thus,  output  that  was  intended  to come out  with pre-spacing will
instead  be  printed (after the >COPY)  with post-spacing; needless to
say,  the  output will end up looking  very different from what it was
supposed to look like.

What  can you do about this? Well,  I recommend that you take the disc
file  generated by the >COPY and add  one record at the very beginning
that  contains  only a single letter "A"  in its first character. This
"A"  is the carriage control code for switch-to-pre-spacing, and it is
the  very  code  that  (if I've diagnosed  your problem correctly) was
dropped  by  the  >COPY  command.  By re-inserting this  "A" code, you
should be able to return the output to its original, intended format.

Now,  this is only a guess -- I'm just suggesting that you try putting
in  this "A" line and seeing if the result is any better than what you
had  before. There might be other carriage control codes being lost by
the  >COPY; there might be, for instance, carriage control transmitted
using  FCONTROLs,  or  your  program might even  switch back and forth
between  pre-  and  post-spacing  mode  (which  is  fairly  unlikely).
However, the lost switch-to-pre-spacing is, I believe, the most common
cause of outputs mangled by the SPOOK >COPY command.

Robert  M.  Green  of Robelle Consulting Ltd.  once wrote an excellent
paper  called  "Secrets  of Carriage Control"  (published, among other
places,  in  The  HP3000  Bible,  available  from  PCI Press  at (512)
250-5518);  it  explains  a  lot of little-known  facts about carriage
control  files,  and  may  help  you  write programs  that don't cause
"interesting"  behavior  like  the  one  you've  experienced. Carriage
control  is  a tricky matter, and Bob  Green's paper discusses it very
well.


Q: Please help me resolve a problem I am having attempting to use only
a  function  key  to  issue a :HELLO command.  I know about the escape
sequence  that will define a user function key with the characters for
the  :HELLO;  the  problem  I'm having is that  the RETURN key must be
entered  first  before  the character string from  the function key is
accepted.  This  might  not  sound  like  much  of  a problem  but the
combination  of  pressing  the  RETURN key followed  by a function key
would  be too confusing for the users  to follow. What I would like to
do would be to have the users sign on by pressing only a function key.
Security  is done by the application,  including passwords, so I don't
think  that  I'm  taking  that  much of a security  risk by having the
:HELLO command stored in a function key.

What  is apparently happening is that  the RETURN key triggers the CPU
to  accept  a  response  from  the  terminal  using the  DC1, DC2, DC1
protocol;  what  I can't accomplish is  duplicating this pattern as an
escape sequence in the function key definition. The best that I can do
is  to  include  the  escape sequence for the  RETURN key as the first
character  in the string. This will  make the CPU accept the remaining
portion  of  the  character  string  but  the  time  delay  causes the
beginning  characters of the HELLO command  to be missed. I have tried
other  escape patterns without any success.  Please do what you can to
help me resolve this problem.

A: The ozone layer is dissolving, crime is rampant in the streets, and
the  Khmer Rouge are about to regain power in Cambodia -- and THIS you
call  a  problem?  Well,  I suppose it is,  after a fashion, and being
unable  to  do anything about the first  three problems I mentioned, I
did some research into yours.

Since  I  know  absolutely  nothing  about  this  sort of  issue (MPE,
languages,  and the File System being  more my specialty), my research
led  me  to the people who know  EVERYTHING about terminals talking to
the  HP3000,  namely  Telamon, Inc. in Oakland.  Randy Medd of Telamon
(one  of  their two technical gurus, the  other being, of course, Ross
Scroggs)  was  kind  enough  to  answer the question,  and here is his
reply:

   The  only  apparent solution to this  problem involves, among other
   things,  the necessity of hitting the function key twice, e.g. [f1]
   [f1]. Here is how this can be done:

     1. Make sure that the logon port is configured as Speed Specified
        (i.e.   NOT   Speed-Sensing)  --  either  sub-type  4  (for  a
        direct-connect)  or  5  (for a modem port).  This, by the way,
        requires  that the terminal's configured speed EXACTLY matches
        the port's default speed.

     2.  Define  the function key as an  'N' (Normal) type key with an
        explicit carriage return terminating the sequence, e.g.

          "[ESC]&f1k0a16d019LLog On          HELLO USER.ACCOUNT[CR]"

        (where  [ESC] is an ASCII 27 and [CR] is an ASCII 13). Be sure
        that "k0" is specified, not "k1" or "k2".

     3. The first press of this function key will cause the ":" prompt
        to  appear and the second press will cause the HELLO string to
        be  sent to MPE, although the user must still wait for the ":"
        before pressing the function key the second time.

   The  reason why this won't work on a Speed-Sensed port (sub-types 0
   and/or  1)  is that the speed-sense  algorithm requires some "dead"
   time  both  before  and  after  the  carriage  return  in  order to
   determine  the  speed. If the  aforementioned function key sequence
   were  sent to a speed-sensed port, no ":" would be generated as too
   many   characters   arrived  too  soon  BEFORE  the  first  CR  was
   encountered.  If  the  function  key  were  defined with  a leading
   carriage return, in addition to the HELLO ... [CR], no ":" would be
   generated  as  the  terminal  driver would see  too many characters
   arriving  too soon AFTER the initial CR. Note that this synopsis is
   based  upon observation, not upon inspection of the actual terminal
   driver code itself.

   If   the   port   is  Speed-Specified,  the  additional  characters
   encountered  during  the  first  transmission  of  the key  will be
   ignored  as the driver can instantly recognize each character as it
   arrives, discarding characters until the first CR is found.

   Even   the  addition  of  type-ahead  (via  a  Type  Ahead  Engine,
   Reflection, or Advance Link) won't solve this problem. All three of
   these products initially assume that their type-ahead function will
   not  be activated until the initial DC1 read trigger is encountered
   from  the HP3000. Programming a function key with a leading CR will
   fail  on  a Speed-Specified port because  the entire function key's
   contents  will almost certainly be transmitted to the HP3000 before
   the  computer gets around to sending back the initial CR+LF+":"+DC1
   prompt for the logon (although the [F1] [F1] sequence should work).
   This  sequence will fail on a Speed-Sensed port for the same reason
   mentioned  above -- no ":"+DC1 will be generated as the speed-sense
   algorithm  won't  recognize  any  CRs  sent  with  text immediately
   preceding or following.

So,  there  it  is, thanks to Randy. It seems  to me that you might be
better  off  convincing  your  users that they  can, after all, master
hitting  the  RETURN  key before using the  function key -- otherwise,
you'll  have  to  reconfigure your ports  to be Speed-Specified, which
will  be  at  least a bother and perhaps  a serious problem, since you
would  then have to reconfigure the system any time you want to change
a terminal baud rate (which, to be frank, should be rather rare).

If  you  MUST  avoid  that  leading RETURN key,  it seems that Randy's
double-hit-of-the-function-key solution is your best bet. Good luck.


Q:  When  we  got  V-delta-4,  we  started  to use  the Access Control
Definition  (ACD)  feature,  which  finally let us  restrict access to
files  to specific users. We were very  happy with it, and in fact set
up ACDs for thousands of our key files. Everything went well until our
next  reload  -- after the reload was done,  we had found that all the
ACDs had VANISHED!!! What happened???

A:  If you take a careful look at the V-delta-4 Communicator, you will
find  (among  the  twenty  or so pages devoted  to ACDs) the following
paragraph (on the bottom of page 3-2):

   If  the  MPE V/E command :SYSDUMP is  used to perform a backup, the
   ;COPYACD parameter must be specified in the dump file subset:

     :FILE T;DEV=TAPE
     :SYSDUMP
       Any changes?
       Enter dump date? 0
       Enter dump file subset
       @.@.@;COPYACD

Also, at the bottom of page 3-16, HP says:

   To  backup  files with ACDs in  :SYSDUMP, the ;COPYACD keyword will
   need to be specified with the fileset information.

If  you  do  NOT specify ;COPYACD on a  :SYSDUMP (or on a :STORE), THE
ACD'S  OF THE STORED FILES WILL *NOT* BE WRITTEN TO TAPE. Then, if you
:RESTORE  or  RELOAD  from the tape, the  ACDs will be COMPLETELY LOST
(since there'll be no place for the system to find them).

So,  there it is -- as Alfredo  Rego once put it, documentation should
be  read  like  a  love letter, with attention  paid to every dot over
every  'i'  and  every crossing of a 't'.  If you miss those couple of
paragraphs, you lose all your ACD information the next time you reload
or :RESTORE from a backup.

You may well ask: If ;COPYACD is so vital for all ACD users, WHY LEAVE
IT  AS AN OPTION??? Well, HP had a reason for it: compatibility. Tapes
with  ACDs  on  them  are  NOT  readable by  pre-V-delta-4 versions of
:RESTORE  (or of the RELOAD option of the system boot facility). If HP
had  defaulted  all  post-V-delta-4 :STOREs and  :SYSDUMPs to have the
;COPYACD  option, then you might have  run into problems the next time
you  tried  to  transport  a  post-V-delta-4  tape to  a pre-V-delta-4
machine.  I'm  not sure that this would  be a more severe problem than
the total loss of all ACDs that may happen with the default being what
it is now, but I can certainly see HP's point.

So,  if  you  use ACDs, you MUST put  a ;COPYACD on your system backup
command,  but realize at the same time  that this will make your tapes
unreadable on pre-V-delta-4 systems.

The  :FULLBACKUP  and :PARTBACKUP commands,  incidentally, will always
create  a  backup  tape  with  the  ;COPYACD  option. This  will avoid
confusion with lost ACDs, but, as we just pointed out, will make their
backup tapes untransportable to pre-V-delta-4 machines. You can't have
everything.


Q:  I'm  sick  and  tired  of  having  to  manually  look  up  all
those  CI  and  file  system error numbers that  my programs print out
whenever  they get a COMMAND intrinsic or file system intrinsic error.
How  can  I  make  them  print  the error MESSAGE,  not just the error
NUMBER?

A:  I agree wholeheartedly; I got  tired of manually looking up errors
years  ago  (especially  since  it's  hardly  easy  to look  them up).
Fortunately,  there  is  a  --  relatively  -- straightforward  way of
turning  those cryptic numbers into  (sometimes equally cryptic) error
messages.

On MPE/V, the intrinsic that you want is called GENMESSAGE. Its job is
to output an arbitrary message from an arbitrary "set" of an arbitrary
message  catalog  file. Actually, you can use  this to create your own
catalog  files  with  your own programs' error  messages, but for your
particular problem you need to use the file CATALOG.PUB.SYS.

CATALOG.PUB.SYS  is where MPE keeps virtually all the messages that it
can  output to you -- console messages, CI error messages, file system
error  messages, loader error messages, etc.  (You can even modify the
text  of  these messages by modifying  the CATALOG.PUB.SYS file -- see
Chapter  9  of  the  MPE  V  System Operation  and Resource Management
Manual.)  To print a particular message  from this file, you must open
this file MR NOBUF and then call the GENMESSAGE intrinsic, passing the
file number, the error number whose message you want to print, and the
"set number", which is 2 for CI errors and 8 for file system errors.

Thus, here's what a sample error message printing procedure might look
like  (in  this  example,  it's  in  FORTRAN,  but it could  be in any
language):

   SUBROUTINE PRINTMPEERROR (FNUM, ERRNUM)
   INTEGER FNUM, ERRNUM
   SYSTEM INTRINSIC FOPEN, GENMESSAGE
   IF (FNUM .NE. 0) GOTO 10
     FNUM = FOPEN ("CATALOG.PUB.SYS ", 1, %420)
10 CALL GENMESSAGE (FNUM, 2, ERRNUM)
   RETURN
   END

If   you   pass  it  a  file  number  of  0,  it  automatically  opens
CATALOG.PUB.SYS  and  returns its file  number; subsequent calls won't
require  CATALOG.PUB.SYS to be re-opened. A similar procedure could be
written  to  print a file system error  message; it would just have to
pass an "8" instead of a "2" to GENMESSAGE.

2  and  8  are  the  arbitrary  set  numbers  that  the  designers  of
CATALOG.PUB.SYS   chose   for  MPE  errors  and  file  system  errors,
respectively;  they're the most useful of the more than 20 sets in the
file,  but you can find out about the other message sets by looking at
all the lines in CATALOG.PUB.SYS that start with "$SET ".

What  about MPE/XL? The GENMESSAGE intrinsic is still the right way to
output  CI errors and FOPEN errors on MPE/XL; however, many intrinsics
(such  as  HPFOPEN,  HPCIGETVAR,  and others) return  a 32-bit "status
word"  that  GENMESSAGE  can't  handle. To output  this sort of status
word, you should call the new HPERRMSG intrinsic, e.g.:

   PROCEDURE HPERRMSG; INTRINSIC;
   ...
   HPERRMSG (2, 0, 0, STATUS);

(This  example  is  in  PASCAL/XL,  but HPERRMSG is  callable from any
language.)  The  "2" simply indicates that the  error message is to be
printed  to  the  terminal;  there's no need to  specify a set number,
since the MPE/XL 32-bit status contains within itself an indication of
what kind of error this is.

What if you've been given an error number -- a CI error, a file system
error,  or  an  MPE/XL  32-bit  status  --  and  want to  find out the
corresponding  error message without writing a program? There are some
contributed  programs  that  do  this;  you can also  (for CI and file
system errors) say:

   :FCOPY FROM=CATALOG.PUB.SYS;TO;SUBSET="1234 "

This  will  show you all the lines  in CATALOG.PUB.SYS that start with
(in  this example) "1234 ", which  will include all the error messages
(CI, file system, loader, etc.) corresponding to error #1234; a clumsy
approach,  but better than nothing. (Of  course, you can also use your
favorite editor for this.)

If you use MPEX/3000, you can say

   %CALC ABORTMPEF (cierr, fserr)

which  will  output  the error messages corresponding  to the given CI
error  number and file system error number; if you only want to output
one of these errors, you can set the other parameter to 0, e.g.

   %CALC ABORTMPEF (0, 74)

to  get  the  message  for  file system error 74.  (Don't ask me why I
called this function ABORTMPEF instead of something sensible.)

An  upcoming  version  of MPEX/3000 will  also have a WRITEMPEXLSTATUS
function that will let you say (on MPE/XL)

   %CALC WRITEMPEXLSTATUS (statusvalue)

to format an MPE/XL status.


Q:  Suppose  I  want  an  application  to  access  the  :RUN ...;INFO=
information.  I  would  like to use the  GETINFO system intrinsic, but
only  if  it is available (since  some early-MPE/V and MPE/IV machines
don't  have  it). If GETINFO isn't available,  I don't mind not having
access  to the ;INFO= string (and the ;PARM= value), but I do want the
rest  of  the  program to run; however, if  I just call GETINFO and it
isn't there, my program won't even load.

I  suspect that what I want can  be accomplished with the LOADPROC and
UNLOADPROC system intrinsics, but I don't know exactly how.

A:  You're right -- LOADPROC is the  very thing that you need, in this
case,  or in any other case in which you want to call a procedure that
may  or may not be there (for example, a new MPE/XL procedure [such as
the  Switch procedure, HPSWTONMNAME], if you have both an MPE/V and an
MPE/XL machine).

Calling the LOADPROC intrinsic is, however, the easy part. You pass to
it  the  procedure name (a byte array,  terminated with a blank) and a
library identifier (0 indicates to load from SL.PUB.SYS) -- it returns
to  you  an  "identity  number"  (which  you  can  use  when  you call
UNLOADPROC)  and  the  procedure's "plabel". This  much the Intrinsics
Manual  will  tell  you;  what  it  won't tell you is  HOW TO CALL THE
PROCEDURE once you've loaded it!

That's  right -- LOADPROC doesn't actually call the procedure (how can
it  if you haven't passed the procedure parameters?), but just returns
the  procedure's  "plabel".  The  trick now -- and  it's a very tricky
trick  --  is  to  use  this  plabel  to  call the  procedure. This is
something  that you have to do in SPL, and is one of the rare times in
which  you MUST use the low-level ASSEMBLE and TOS constructs that the
SPL compiler gives you.

To  call  a  procedure  given  its plabel, you must  set up all of the
procedure's parameters on the stack in EXACTLY the way that a compiler
would  if  you called the procedure  normally from the compiler; then,
you must push the plabel onto the stack (say TOS:=PLABEL) and call the
procedure (using ASSEMBLE (PCAL 0)).

This is best shown by an example:

   INTEGER PROCEDURE MYGETINFO (INFO, INFOLEN, PARM);
   BYTE ARRAY INFO;
   INTEGER INFOLEN, PARM;
   << Returns 0 if all went well, >>
   <<         1 if GETINFO failed or couldn't be loaded. >>
   << To call from COBOL, be sure to specify an "@" before >>
   <<         the INFO parameter, and declare INFOLEN and PARM >>
   <<         as PIC S9(4) COMP. >>
   BEGIN
   INTRINSIC LOADPROC, UNLOADPROC;
   BYTE ARRAY PROC'NAME(0:15);
   INTEGER IDENT, PLABEL;
   MOVE PROC'NAME:="GETINFO ";
   IDENT:=LOADPROC (PROC'NAME, 0, PLABEL);
   IF <> THEN
     MYGETINFO:=1
   ELSE
     BEGIN
     TOS:=@INFO;
     TOS:=@INFOLEN;
     TOS:=@PARM;
     TOS:=%7;    << guess what this means >>
     TOS:=PLABEL;
     ASSEMBLE (PCAL 0);
     MYGETINFO:=TOS;
     UNLOADPROC (IDENT);
     END;
   END;

The  LOADPROC  call  is  simple;  if it fails  (condition code <>), we
return immediately with a result of "1". What if it succeeds?

   * We  push the three GETINFO  intrinsic parameters onto the stack.
     Note  that  we  say  "TOS:=@varname"  --  this means  to push the
     ADDRESS  of  the given parameter onto the  stack; we must do this
     because  all  three of the GETINFO  intrinsic's parameters are by
     REFERENCE.  If  they were by VALUE, we'd  just push them onto the
     stack by saying "TOS:=varname" or "TOS:=expression".

   * We  push  a  7 onto the stack; the  7 is the GETINFO intrinsic's
     OPTION VARIABLE MASK. The GETINFO intrinsic is marked as "O-V" in
     the  Intrinsics  Manual, which means that  some of its parameters
     may  be omitted. Even though we don't  omit any in this call, the
     intrinsic  does expect -- at the very top of the stack -- the bit
     mask that indicates which parameters are actually being passed.

     The  lowest-order  bit  (bit  15)  indicates the  presence (1) or
     absence   (0)  of  the  LAST  parameter;  the  next  lowest  (14)
     corresponds  to  the  SECOND TO LAST; and so  on. The 7 (in which
     bits  13, 14, and 15 are set)  simply means that all 3 parameters
     are passed.

     The  few  procedures  that take more than  16 parameters expect a
     TWO-word  (32-bit)  OPTION  VARIABLE mask; procedures  with 16 or
     fewer parameters expect just a one-word mask.

   * We push the PLABEL onto the  stack and then do an ASSEMBLE (PCAL
     0)  -- the "PCAL 0" means "call  the procedure whose plabel is on
     top of the stack". This actually calls the GETINFO intrinsic.

   * Finally,  since GETINFO returns a  result, the result remains on
     the  stack  after the PCAL 0;  the MYGETINFO:=TOS pops this value
     into the MYGETINFO procedure result.

So,  there  you  go; all we need to do  now is an UNLOADPROC and we're
done.  Of course, as I mentioned, this can only be done in SPL (unless
you know of a TOS or ASSEMBLE operator in COBOL!); and, each procedure
you  want to call this way  requires some special thinking, since it's
very  easy  to make a mistake in the  way you push the parameters onto
the stack.

However,  once  you master this, you can  write programs that can take
advantage  of  powerful features of newer  versions of MPE while still
remaining  compatible  with  older  versions.  Even  if  all  of  your
computers  have  the GETINFO intrinsic (the  great majority of HP3000s
now  do),  this  can still be relevant for  any new intrinsics that HP
comes  out  with,  for  instance  HPACDPUT  or HPACDINFO  on MPE/V, or
HPSWTONMNAME on MPE/XL.

This  mechanism is also quite useful when you want to call a procedure
whose  name  is  not  known  at  the time you  write your program; for
example, EDITOR's and QEDIT's /PROCEDURE commands let the user specify
the  name  of  his own procedure that  can be called for sophisticated
editing  operations.  One  thing  that you have  to remember for this,
though,  is  that  you  have  to firmly dictate to  the user the exact
calling  sequence  of the procedure that he  is to write, since you'll
have to properly stack all the parameters for the procedure call.

Finally,  note  that on MPE/XL in Native  Mode the mechanism for doing
this   is   substantially   different,   and   substantially  simpler;
PASCAL/XL's  CALL  and  FCALL  built-in  functions and  the UNRESOLVED
procedure  attribute  let you do all this  without any ASSEMBLE or TOS
mumbo-jumbo.

Go to Adager's index of technical papers