Q L H A C K E R ' S J O U R N A L
===========================================
Supporting All QL Programmers
===========================================
#29 October 1998
The QL Hacker's Journal (QHJ) is published by Tim
Swenson as a service to the QL Community. The QHJ is
freely distributable. Past issues are available on disk,
via e-mail, or via the Anon-FTP server, garbo.uwasa.fi.
The QHJ is always on the look out for article submissions.
QL Hacker's Journal
c/o Tim Swenson
2455 Medallion Dr.
Union City, CA 94587
swensont@geocities.com swensont@jack.sns.com
http://www.geocities.com/SiliconValley/Pines/5865/index.html
EDITOR'S FORUMN
I'd like to thank Per Witte for providing pretty much the
core part of this issue. I saw the Filename Parser program
posted to the ql-users mailing list and thought it would be
good material for an article. I contaced Per and he was
willing to write something up. The length of the article
was more that I expected, but I'm not complaining.
I'm still pressing forth with the Qlib Source Book. I've
been delayed by a broken hand (out of cast and getting
better). I'm pushing myself to get something out soon, even
if only about 10% of what I would like to see in the Book.
I'm looking for information about the history of Qliberator
(the various releases) and any bugs in the current release
(3.36). If you have some info, please send it to me.
My other project is the QL PD Documentation Project (I'm
using PD loosly to mean freely available). I'm trying to
collect (in one location) a variety of released information
about the QL and QDOS. Currently I have a number of
documents, including parts of the QL User Guide, a couple of
Tutorials, and a few articles that I've scanned in from
IQLR. Please take a look at my web site to see what I've
gathered. It is:
www.geocities.com/SiliconValley/Pines/5865/
SHELLING OUT TO SUPERBASIC
The QL is unique in how QDOS and SuperBasic are sort of
rolled into one. Just as we can only have one copy of QDOS
running, we can only have one copy of SuperBasic running
(OK, I know this is not true for Minerva).
In a traditional operating system model, there is the OS and
the Shell. The Shell is a user interface to the operating
system. In UNIX there are many different shells; Boune
(sh), C (csh), Korn (ksh), etc.. In MS-DOS the shell was in
COMMAND.COM. Even with Win95, there is a MS-DOS Shell still
available.
In most OS's, there can usually be more than one shell
ruuning at one time. Application programs can fire off
another Shell and run one or more commands. The new shell
becomes a child process of the calling application.
All of this comes about because the shell is nothing more
than another user application. On the QL we are limited in
that only one copy of SuperBasic can be run at one time at
it must be Job 0.
In QDOS, user applications can call other programs. An
editor could call C68 to compile a program that was just
edited and saved. But when the command needed to run is a
built-in SuperBasic command (which includes loadable
extensions), thete is no way to shell out to SuperBasic to
run the command.
I ran into this problem when I wanted to run Qliberator from
within MicroEmacs. MicroEmacs allows for executing programs
from MicroEmacs, but I could not run all the command for
QLiberator that I needed. To explain, here are the steps to
run QLiberator:
1. Load the program into SuperBasic
2. Call "Liberate" to create a working file.
3. Execute Qlib
Here is a the steps in SuperBasic terms:
LOAD FLP1_TEST_BAS
LIBERATE FLP1_TEST_BAS
EXEC QLIB
Since I can't call LOAD or LIBERATE from within MicroEmacs,
I thought I could write a SuperBasic program and then
compile it. This may work with LIBERATE, but LOAD is not a
command that QLib will compile.
I thought I was stuck until I started reading the HotKey
System II manual. It was there that I ran across the two
commands, HOT_CMD and HOT_DO.
HOT_CMD assigns to an ALT key, a command that will be
entered in the SuperBasic window (#0). It does not have to
be an executable program, but can be a SuperBasic statement
or a resident command ( like TKII or DIYtoolkit). HOT_CMD
"picks" the SuperBasic interpreter to the top and the
command is sent to SuperBasic to be run.
The HotKey System II was designed to be driven by the user.
It is the user typing in a HotKey sequence that puts the
System into motion. There in a HotKey command that allows
automation of the HotKey System: HOT_DO. HOT_DO tells the
System to implement a HotKey. HOT_DO('a') is the same as
the user hitting the ALT-a keys.
Using HOT_CMD and HOT_DO in conjunction, the programmer can
perform actions just as if they were "shelling" out to
SuperBasic. In the case stated above, here is how I would
use the two commands to automate using Qliberator:
10 file$ = "flp1_test_bas"
20 ERT HOT_CMD('a','LOAD file$')
30 HOT_DO('a')
40 ERT HOT_CMD('b','LIBERATE file$')
50 HOT_DO('b')
60 EXEC QLIB_OBJ : REMark Just EXEC it.
I can get away with having LIBERATE in a compiled QLIB
program, but not LOAD. Here is a way to get around this
limitation.
PARAMETER PASSING TECHNIQUES IN S*BASIC
Per Witte
In QHJ#24 and #25 there were articles on parameter passing
techniques (By Tim Swenson and by Peter Tillier,
respectively). I won't (too much) re-hash what's already
been said (this article was prepared before I was aware of
the other two), but look at the matter in a practical way,
that may suit some readers.
I'm using SBASIC for this article - the enhanced SuperBASIC
interpreter that comes with SMSQ and SMSQ/E. SBASIC behaves
somewhat differently to SuperBASIC with respect to variable
handling, and has some desirable features, not available in
standard SuperBASIC. Where this affects the subject at hand
I shall try and point out the differences. However, I am
presently not able to test my examples in SuperBASIC, so
some incompatibilities (ie, bugs) may be found. Please
always ensure that the techniques described work with your
version of S*BASIC before relying on them in any way.
<<< Value or Reference ? >>>
There are two ways of passing parameters to functions and
procedures in S*BASIC: by value, which is perhaps the
"intuitive" method; and by reference, which will be the main
focus of this article.
Passing parameters by value is (what we may THINK) we
normally do. RUNning the program fragment below
10 test1 1,2,3
99 :
100 defproc test1(a,b,c)
110 print a,b,c
120 enddef
130 :
would print "1 2 3" on your screen (with any luck!). And of
course:
10 x=1:y=2:z=3: rem Assign values to some variables
20 test1 x,y,z: rem and use these instead.
does the same. But a small modification of test, test2,
shows what's really going on:
99 :
100 defproc test2(a,b,c)
110 print a,b,c
120 a=a+a:b=b+b:c=c+c:rem Double all parameter variables!
130 enddef
140 :
The new harness:
10 x=1:y=2:z=3
20 test2 x,y,z
30 print x,y,z
1 2 3 <- prints out x, y, & z, as expected
2 4 6 <- but what's going on here? We set x,y,z to
be 1,2,3!
By altering the values of the parameter variables a, b, & c,
we cause a change to the calling variables x, y, & z too.
This is a call by reference; we don't pass to the procedure
merely the values the variables contain, instead we refer to
the original variables - a, b & c ARE x, y, z, only by a
different name. As you will appreciate passing parameters by
reference is not always desirable. In fact, unless you
specifically want to do so, it could be a real pain: You can
see how a procedure might easily (and unintended by you)
alter its parameters, and thereby variables external to
itself. To avoid this you can apply the rule never to alter
a procedure's formal parameters within the procedure; or you
must, pass your variables by value only. But how to do that?
If you typed: test2 1,2,3, what do you think happens to a,
b, c? Well, their values are simply thrown away when the
routine returns. By extension, the same holds good for:
test2 p+1,q+1,r+1, having previously set p, q, r to some
value. Anything other than a variable is considered an
expression in this context, and can therefore not receive a
return value.
Thus:
10 x=1:y=2:z=3
20 test2 (x),y+0,z^1
30 print x,y,z
1 2 3 <- prints out x, y, & z, as expected
1 2 3 <- prints out x, y, & z, as expected(?)
Good programming practice would avoid altering the parameter
variables - copy their values into LOCal variables instead!
In my opinion, test2 (x),(y),(z) gives the clearest
indication of intent, besides being (marginally) faster than
say, x+0,y+0,z+0, and so is a good convention to adopt for
call by value.
<<< Coersion >>>
There are other "oddities" about the way parameter passing
works. For example:
10 x$='a':y=3:z%=3
20 test3 x$,y,z%
30 print x$,y,z%
99 :
100 defproc test3(a,b,c)
110 print a,b,c
120 a = a & a : b = b / 2 : c = c / 2
130 enddef
140:
RUNning this program produces:
a 3 3 <- x$ y z%
aa 1.5 2
Not what you'd think, looking at the formal parameters a, b,
c! However, this can be very useful, as will be shown later.
Things to watch out for though are: You may assume that the
formal parameter decides the type, when it actually is the
calling parameter that does so! An example might be:
10 x=1:y=420 fast_test x,y,10
99 :
100 defproc fast_test(a%,b%,c%)
110 rep loop
120 a%=a%+1:b%=b%+b%:c%=c% div 3
130 if c%=0:exit loop
140 endrep loop
150 enddef
160 :
You're expending all this effort optimising fast_test;
changing out floating point variables with integers, and the
like. You need not have bothered! This is what it's actually
doing:
120 a=a+1:b=b+b:c=c div 3
In fact everything runs in (relatively) slow floating point!
The correct moves are:
10 x%=1:y%=4:z%=10
20 fast_test x%,y%,z%
will pass integers to fast_test, and/or
100 defproc fast_test(a%,b%,c%)
110 loc r%,s%,t%
120 r%=a%:s%=b%:t%=c%
etc..
What you then use in the formal parameter list is irrelevent
(except as a reminder as to what the correct type should
be!) Also copying the parameters into LOCal variables will
coerce the parameters back to the desired type. In tk2 there
are commands to test the parameters that are passed to a
procedure: PARTYP tells you the actual parameter type (nul
(never nul in SBASIC), string, float, or integer) and PARUSE
whether the parameter is an array or not.
<<< Returning Values through the Parameter List >>>
A "by-product" of the ability to pass parameters by
reference, is that we can actually return more than one
value to the calling program. Both functions and procedures
can be used for this. I find the
function_error = Function(update-able parameter list)
construct particularly useful, as I hope to show. Below
follows a commented listing on a filename parsing utility
for S*BASIC that hopefully illustrates the technique:
1 PRINT,'(Simplyfied) Filename Parser'
2 REMark ©PWitte 1998
3 PRINT,!!!!!'PD - No Warranties'!!!!!
4 :
5 dfnm$='win1_bas_util_fnm_ParseFnm_bas'
6 er=ParseFnm(dfnm$,ddev$,ddir$,dnm$,dext$)
7 PRINT\\'Fnm:'!dfnm$\\'Dev:'!ddev$\'Dir:'!ddir$\
8 PRINT 'Nme:'!dnm$\'xt:'!dext$\'Err:'!er
9 STOP
10 :
Above. The first part of the program gives some information,
and shows and example of usage.
32724 DEFine FuNction ParseFnm(f$,v$,d$,n$,e$)
This part of the program is the object of the enterprise;
the file name parser itself. Due to the nature of the QL's
file system (FS), it is impossible to determine how much of
the latter part of the name is filename and how much is
directory name merely by inspecting the filespec. You have
to actually open the file (or its directory) to find out.
The function does this, and then breaks up the filespec
according to a mixture of known facts and assumptions (ie,
it's not foolproof!) It puts the different sections into the
supplied variables, and returns ok.
The function is defined as a floating point function, even
though its main task is to manipulate, and you might say,
return text. In this, simplified, version any values
pre-supplied in v$..e$ are overwritten. The only parameter
you should supply is the f$ (for Full Filespec) This is
(more or less) expected to be in the form of:
key: <> = name; | = or; [] = optional (0..1);
{} = repeated (0..)
= directory separator, '_'
= extension separator, '_ | .' (SMSQ/E)
=
{}
[[[]]]
32725 LOCal c,t,p%,i%
32726 REMark Split filename into components
32727 c=FOP_DIR(f$):IF c<0:RETurn c
FOP_DIR is a function (introduced in Toolkit I/II (tk2), by
Tony Tebby, and included with many disk interfaces, and in
SMSQ*). It tries to make the best of the information
supplied, and will open the first directory that matches the
first part of the filespec. So if you have a directory
called 'win1_asm_' (but none called 'win1_asm_prg_...') and
you did a ERT FOP_DIR(win1_asm_prg_temp) the function would
open directory 'win1_asm_' taking the rest of the filespec
to be a filename!
32728 d$=FNAME$(#c):CLOSE#c
FNAME$ (also a function from tk2) returns the name of any
file, also directory files. So, continuing our specific
example above, d$ (for Directory) would now contain 'asm' -
Note the device name is not returned.
32729 IF LEN(d$) THEN
FNAME$ did return (at least the first) part of the directory
name, eg 'asm'.
32730 p%=d$ INSTR f$:IF p%=0:RETurn -7
If the filename returned by FNAME$ is, after all, not in the
filespec return the error Not Found.
(This would be the case if you tried to:
DATA_USE 'win1_asm'
ERT FOP_DIR(#3;'abc_test')
FNAME$(#3) would then return 'asm')
32731 d$=d$&'_'
If d$ _is_ a substring of filespec, append the filename
separator (as the last one is not stored in the directory
file).
32732 ELSE
At this point d$ is ''. This could mean that had been
specified; that no matching directory was found (eg had we
specified 'win1_prog_temp_..' and there was no
'win1_prog_..' ); or that something was wrong.
32733 p%=('_' INSTR f$)+1
Do a primitive test on the filespec to see if it contains a
devicename, eg 'win1_..'.
32734 END IF
32735 v$=REMV$(p%,LEN(f$),f$)
v$ stands for deVicename. v$ gets set to the first part of
filespec, up to the first underscore.
32736 IF LEN(v$)<3:RETurn -12
Better would have been:
32735 IF p%<3 OR p%>5:RETurn -12
This version of the filename parser doesn't support
networked drives, so:
32735 IF p%<>5:RETurn -12
would be correct here. Then:
32736 v$=REMV$(p%,LEN(f$),f$)
Tests whether the first part of the filespec is a possible
devicename. (Devicenames can only legally be 3, 4, or 5
charcters long, as in: 'S7_', 'n63_', 'ram2_'. Anything
other is an error. Further tests should be done here to
determine whether v$ is 'legal' device name, but there is no
easy way of knowing for sure. (Try: OPEN_NEW#3;
'flp7_test':PRINT FNAME$(#3) and see what you get (presuming
you don't have an flp7_ ;)
32737 IF p%+LEN(d$)=LEN(f$) THEN
32738 n$='':e$=''
We allow filespec to be incomplete from devicename down. In
the case above filespec == device name & directory name, ie
there is no filename and no extension.
32739 ELSE
32740 n$=REMV$(1,p%+LEN(d$)-1,f$)
There is a name (and possibly an extension). Let n$ (for
fileName) hold it for now.
32741 p%=0
32742 FOR i%=LEN(n$) TO 1 STEP -1
32743 IF n$(i%) INSTR '_.':p%=i%:EXIT i%
32744 END FOR i%
Here we just check filename from the end of the string for
the first '.' or '_' it encounters. This, it decides, will
be the extension.
32745 IF p%=0 THEN
32746 e$=''
No extension found.
32747 ELSE
32748 e$=REMV$(0,p%-1,n$)
32749 n$=REMV$(p%,99,n$)
Slice filename into name part and extension part.
32750 END IF
32751 END IF
32752 RETurn 0
Return OK.
32753 END DEFine
32754 :
The final part of the program is a help-function REMOVE$
(shortened to REMV$ in its S*BASIC incarnation) all it does
is to simplify string slicing by encapsulating all the error
trapping. It won't be looked at here.
32755 DEFine FuNction REMV$(fr%,to%,str$)
32756 IF fr% < 2 THEN
32757 IF to% >= LEN(str$):RETurn ''
32758 RETurn str$(to% + 1 TO LEN(str$))
32759 END IF
32760 IF to% >= LEN(str$) THEN
32761 RETurn str$(1 TO fr% - 1)
32762 ELSE
32763 RET str$(1 TO fr% - 1) & str$(to% + 1 TO LEN(str$))
32764 END IF
32765 END DEFine
32766 :
The weird numbering scheme is to enable the function to be
easily MERGEd into a larger program that needs it;
linenumbers <100 can be removed after testing.
<<< Arrays as Parameters >>>
Also arrays are passed by reference; when you supply an
array parameter you are allowing the procedure to access
your actual array. The same rules described above regarding
type coercion also apply to arrays.
Unfortunately, S*BASIC provides only limited "mass"
operations (for lack of a better term) on arrays though you
can pretty much slice them up any which way you choose. This
comes in handy if you want to write your own mass-ops in
S*BASIC or machine code.
You can't do a = b with arrays in S*BASIC but you can writeyour own EQU a TO b, which does exactly the same (see
commented listing of EQU below).
1 DIM a$(2,2,2,2,6),b$(2,2,2,2,8)
2 DIM a(2,2,2,2),b(2,2,2,2)
3 FOR i%=0 TO 2:FOR j%=0 TO 2:FOR k%=0 TO 2:FOR l%=0 TO
2:a$(i%,j%,k%,l%)='L'&i%&j%&k%&l%
4 count%=0
5 FOR i%=0 TO 2:FOR j%=0 TO 2:FOR k%=0 TO 2:FOR l%=0 TO
2:a(i%,j%,k%,l%)=count%:count%=count%+1
6 :
Above. Initialise a few test arrays (use plenty of
dimensions :) Note you'll have to modify all integer
FOR-loops to make this program run under plain QL
SuperBASIC!
10 CLS:PRINT a$,\
12 er=EQU(b$,a$)
14 CLS#0:CLS#2:PRINT#2;b$,\:PRINT#0;er
16 BEEP 2000,20:PAUSE
18 CLS:PRINT !a!\
20 er=EQU(b TO a)
22 CLS#2:PRINT#2!b!\:PRINT#0;er\
24 BEEP 2000,20
26 :
Test harness. Edit to taste.
100 REMark EQU SBASIC function to
101 REMark EQUate two arrays of the
102 REMark same dimensions and type
103 REMark Requires tk2 or equivalent
104 :
105 REMark PWitte, August 1998
106 REMark For "educational" purposes only
107 REMark Use at own risk. No warranties!
108 :
Can't say you haven't been warned!
1000 DEFine FuNction EQU(a,b)
The idea is to equate array a with array b in a reasonably
rational manner, while demonstrating some of the niceties of
parameter passing techniques using arrays, at the same time.
The first thing to note is that EQU will handle any type of
array ie, interger, sting, & float - although the parameter
list only shows float! Also, any number of dimensions are
handled. The only provision, in this implementation, is that
they are of the same type and have the same number and size
of dimensions (except string, in the last dimension).
1010 LOCal er
This nice little feature of "inheritance" is not documented
anywhere, as far as I know:
A LOCal variable defined in one procedure will remain local
in any procedure called by that procedure, unless the
variable has been "re-defined" by a subsequent use of LOCal.
Here the local variable, er (error flag) is set in EQU, the
calling function, and modified in EQN/EQS. Yet a variable
er, defined in the initial code, outside any procedure body,
would retain its original value. The only danger lies in
that if the same sub-routines were to be reused by another
procedure, you may forget to declare it as LOCal in the
calling procedure and end up mysteriously modifying a GLOBal
variable instead!
It certainly saves the the repeated overhead of stacking a
local variable for each recursive call to EQN/EQS here, as
would be the case if we defined er EQN/EQS.
1020 IF PARTYP(a)<>PARTYP(b):RETurn -15
1030 IF PARUSE(a)<>PARUSE(b):RETurn -17
Checks whether paramerters are arrays, and of the same type.
The error checking here is not foolproof.
1040 er=0
1050 IF PARTYP(a)=1 THEN
1060 RETurn EQS(a,b)
1070 ELSE
1080 RETurn EQN(a,b)
1090 END IF
String arrays must be handled slightly differently to
numeric ones, in that the last dimension is the string
itself. It might be possible to find a universal algorithm,
to handle numbers and strings, but it makes sense to use the
built-in mass assignment features and copy whole strings at
once, rather than byte by byte. So the string and numeric
sides have been implented as separate functions. Another
advandage is that this offers the opportunity to optimise
them (how ever slightly) for their different uses.
1100 END DEFine
1110 :
2000 DEFine FuNction EQN(a,b)
2010 LOCal i%
Another reason for separating out these sub-routines as
functions, is that we can take advantage of the
interpreter's excellent array slicing abilities, as in line
2070.
2020 IF DIMN(a)<>DIMN(b):RETurn -4
Every dimension has to match in size. This test will be
performed before any processing takes place. The alternative
would be to use a special sub-routine.
2030 IF DIMN(a(0))=0 THEN
Look-ahead: If the next dimension is past the last, then
this is the dimension we can work with:
2040 FOR i%=0 TO DIMN(a):a(i%)=b(i%)
Copy this dimension from b to a, element by element. This
also terminates recursion at this level.
2050 ELSE
2060 FOR i%=0 TO DIMN(a)
2070 er=EQN(a(i%),b(i%)):IF er<0:EXIT i%
2080 END FOR i%
More than anything, a function like EQU wants speed, so
loops have been specialised, reducing the overheads of test
& branch. The use of recursion is almost necessary, thanks
to the parser's array-slicing abilities!
2090 END IF
2100 RETurn er
The error-checking stuff is not strictly necessary in a
programming toolkit - error-checking is performed at the
program level - but it's easier to delete it than add it
when needed (eg during program development).
2110 END DEFine
2120 :
3000 DEFine FuNction EQS(a,b)
Pretty much the same as for EQN above, but optimised for
string operations.
3010 LOCal i%
Note that the same technique cannot be used for i% as er.
i%'s value will be different at different levels of
recursion ie, i% has to be saved between levels.
3020 IF DIMN(a)<>DIMN(b):RETurn -4
3030 IF DIMN(a(0,0))=0 THEN
This arrangement was actually a bug, as no comparison is
made on the last dimension. However, as the interpreter
doesn't complain and simply ignores any supernumary
characters, I thought I'd leave it there as a feature.
Ie, DIM a$(5,10),b$(5,8): er=EQU(b$,a$)
works, though any strings longer than eight characters will
be truncated.
3030 IF DIMN(a(0,0))=0 THEN
Remember a() refers to an array of type string! Operations
can be performed at a higher structural level, so we
terminate recursion one level up.
3040 FOR i%=0 TO DIMN(a):a(i%)=b(i%)
Copy one whole string at a time.
3050 ELSE
3060 FOR i%=0 TO DIMN(a)
3070 er=EQS(a(i%),b(i%)):IF er<0:EXIT i%
3080 END FOR i%
3090 END IF
3100 RETurn er
3110 END DEFine
3120 :
Genuine bug/incompatibility reports, suggestions and
comments welcome. Send to pjwitte@knoware.nl