Parameter Passing Techniques in S*BASIC

First published in QHJ#29, October 1998

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 them), 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
                                           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 contain a return value, ie, it cannot be altered.

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=2: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 / 5 : c = c / 2
130 enddef
140:
<- - - - - - - - - - - - - - -  - - - - - - - - - - - - - ->

RUNning this program produces:

a     2     3      <- x$   y   z%
aa    .4    1      <-

(Actually SMSQ/E 2.88 returns the wrong value for the integer divide! (2, not 1) This must be a "feature" and will no doubt be fixed in the future.)

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=4:z=10
20 fast_test x,y,z
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 move is:

<- - - - - - - - - - - - - - -  - - - - - - - - - - - - - ->
10 x%=1:y%=4:z%=10
20 fast_test x%,y%,z%
<- - - - - - - - - - - - - - -  - - - - - - - - - - - - - ->

What you then use in the formal parameter list is irrelevent (except as a reminder as to what the correct type should be!)

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. (It can also be used to allow optional parameters in SuperBASIC, as PARUSE will tell you whether the variable is set or not. In SBASIC all variables are pre-initialised to nul).

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

return_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 rem --------------------- cut  here ---------------------|
rem

rem To remove these remarks, and test the program:
rem Cut between - - cut  here - - lines (SBASIC needs the
rem first item to be a line number!) and save to, say: 
rem C:\temp\FnmParse.txt
rem LOAD FnmParse.txt into S*BASIC and then
rem SAVE as say, win1_temp_FileNameParser_bas

1 PRINT,'(Simplyfied) Filename Parser'
2 rem      ©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 :

rem Above. The first part of the program gives some
rem information, and shows and example of usage.

32724 DEFine FuNction  ParseFnm(f$,v$,d$,n$,e$)

rem This part of the program is the object of the
rem enterprise; the file name parser itself. Due to the
rem nature of the QL's file system (FS), it is impossible to
rem determine how much of the latter part of the name is
rem filename and how much is directoryname merely by
rem inspecting the filespec. You have to actually open the
rem file (or its directory) to find out. The function does
rem this, and then breaks up the filespec according to a
rem mixture of known facts and assumptions (ie, it's not
rem foolproof!) It puts the different sections into the
rem supplied variables, and returns ok.

rem The function is defined as a floating point function,
rem even though its main task is to manipulate, and you
rem might say, return text.

rem In this simplified version any values pre-supplied in
rem v$..e$ are overwritten. The only parameter you should
rem supply is the f$ (for Full Filespec) This is (more or
rem less) expected to be in the form:

rem key: <> = name; | = or; [] = optional (0..1);
rem       {} = repeated (0..)
rem
rem   <sep> = directory separator, '_'
rem     <esep>= extension separator, '_ | .' (SMSQ/E)
rem
rem     <filespec> =
rem     <device name><drive number (1..8)><sep>
rem     (<directory section><sep>}
rem     [<filename>[<esep>[<extension>]]]

32725 LOCal c,t,p%,i%
32726 rem Split filename into components
32727 c=FOP_DIR(f$):IF c<0:RETurn c

rem FOP_DIR is a function (introduced in Toolkit I/II (tk2),
rem by Tony Tebby, and included with many disk interfaces,
rem and in SMSQ*). It tries to make the best of the
rem information supplied, and will open the first directory
rem that matches the first part of the filespec. So if you
rem have a directory called 'win1_asm_' (but none called
rem 'win1_asm_prg_...') and you did a
rem
rem ERT FOP_DIR(win1_asm_prg_temp) the function would open
rem
rem directory 'win1_asm_' taking the rest of the filespec to
rem be a filename!

32728 d$=FNAME$(#c):CLOSE#c

rem FNAME$ (also a function from tk2) returns the name of
rem any file, also directory files. So, continuing our
rem specific example above, d$ (for Directory) would now
rem contain 'asm' - Note the device name is not returned.

32729 IF LEN(d$) THEN

rem FNAME$ did return (at least the first) part of the
rem directory name, eg 'asm'.

32730  p%=d$ INSTR f$:IF p%=0:RETurn -7

rem If the filename returned by FNAME$ is, after all, not in
rem the filespec return the error Not Found.
rem
rem (This would be the case if you tried to:
rem             DATA_USE 'win1_asm'
rem             ERT FOP_DIR(#3;'abc_test')
rem FNAME$(#3) would then return 'asm')

32731  d$=d$&'_'

rem If d$ _is_ a substring of filespec, append the filename
rem separator (as the last one is not stored in the
rem directory file).

32732 ELSE

rem At this point d$ is ''. This could mean that <root> had
rem been specified; that no matching directory was found (eg
rem had we specified 'win1_prog_temp_..' and there was no
rem 'win1_prog_..'); or that something was wrong.

32733  p%=('_' INSTR f$)+1

rem Do a primitive test on the filespec to see if it
rem contains a devicename, eg 'win1_..'.

32734 END IF
32735 v$=REMV$(p%,LEN(f$),f$)

rem v$ stands for deVicename. v$ gets set to the first part
rem of filespec, up to the first underscore.

32736 IF LEN(v$)<3:RETurn -12

rem Better would have been:
rem      32735 IF p%<3 OR p%>5:RETurn -12
rem This version of the filename parser doesn't support
rem networked drives, so:
rem      32735 IF p%<>5:RETurn -12
rem would be correct here. Then:
rem       32736 v$=REMV$(p%,LEN(f$),f$)
rem Tests whether the first part of the filespec is a
rem possible devicename. (Devicenames can only legally be 3,
rem 4, or 5 charcters long, as in:

rem 'S7_', 'n63_', 'ram2_'. Anything other is an error.
rem Further tests should be done here to determine whether
rem v$ is 'legal' device name, but there is no easy way of
rem knowing for sure. (Try: OPEN_NEW#3;'flp7_test':PRINT
rem FNAME$(#3) and see what you get (presuming you don't
rem have an flp7_ ;)

32737 IF p%+LEN(d$)=LEN(f$) THEN
32738  n$='':e$=''

rem We allow filespec to be incomplete from devicename down.
rem In the case above filespec == device name & directory
rem name, ie there is no filename and no extension.

32739 ELSE
32740  n$=REMV$(1,p%+LEN(d$)-1,f$)

rem There is a name (and possibly an extension). Let n$ (for
rem 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%

rem Here we just check filename from the end of the string
rem for the first '.' or '_' it encounters. This, it
rem decides, will be the extension.

32745  IF p%=0 THEN
32746   e$=''

rem No extension found

32747  ELSE
32748   e$=REMV$(0,p%-1,n$)
32749   n$=REMV$(p%,99,n$)

rem Slice filename into name part and extension part

32750  END IF
32751 END IF
32752 RETurn 0

rem Return OK

32753 END DEFine
32754 :

rem The final part of the program is a help-function REMOVE$
rem (shortened to REMV$ in its S*BASIC incarnation) all it
rem does is to simplify string slicing by encapsulating all
rem the error checking. 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  RETurn str$(1 TO fr% - 1) & str$(to% + 1 TO LEN(str$))
32764 END IF
32765 END DEFine
32766 :

rem The weird numbering scheme is to enable the function to
rem be easily MERGEd into a larger program that needs it;
rem linenumbers <100 can be removed after testing.

rem ---------------------- cut  here ----------------------|
     

Update: A later version of ParseFnm (without the remarks and hassle) can be had here.

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 write your own EQU a TO b, which does exactly the same (see commented listing of EQU below).

That's it, folks!

1 rem <- - - - - - - - -  cut here  - - - - - - - - - -> mer
rem To remove these remarks, and test the program:
rem Cut between - - cut  here - - lines (SBASIC needs the
rem first item to be a line number!) and save to, say: 
rem C:\temp\EQU.txt
rem LOAD EQU.txt into S*BASIC and then
rem SAVE as say, win1_temp_EQU_bas

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
4  FOR j%=0 TO 2
5   FOR k%=0 TO 2
6    FOR l%=0 TO 2:a$(i%,j%,k%,l%)='L'&i%&j%&k%&l%
7   END FOR k%
8  END FOR j%
9 END FOR i%
10 count%=0
11 FOR i%=0 TO 2
12  FOR j%=0 TO 2
13   FOR k%=0 TO 2
14    FOR l%=0 TO 2:a(i%,j%,k%,l%)=count%:count%=count%+1
15   END FOR k%
16  END FOR j%
17 END FOR i%
18 :

rem Above. Initialise a few test arrays (use plenty of
rem dimensions :) Note you'll have to modify all integer
rem FOR-loops to make this program run under plain QL
rem SuperBASIC!

50 CLS:PRINT a$,\
52 er=EQU(b$ TO a$)
54 CLS#0:CLS#2:PRINT#2;b$,\:PRINT#0;er
56 BEEP 2000,20:PAUSE
58 CLS:PRINT !a!\
60 er=EQU(b TO a)
62 CLS#2:PRINT#2!b!\:PRINT#0;er\
64 BEEP 2000,20
66 :

rem Above: Test harness. Edit to taste.

100 rem EQU SBASIC function to
101 rem EQUate two arrays of the
102 rem same dimensions and type
103 rem Requires tk2 or equivalent
104 :
105 rem   ©PWitte, August 1998
106 rem For "educational" purposes only
107 rem Use at own risk. No warranties!
108 :

rem Can't say you haven't been warned!

1000 DEFine FuNction  EQU(a,b)

rem The idea is to equate one array with another in a
rem reasonably rational manner, while demonstrating some of
rem the niceties of parameter passing techniques using
rem arrays, at the same time.

rem The first thing to note is that EQU will handle any type
rem of array ie, interger, sting, & float - although the
rem parameter list only shows float! Also, any number of
rem dimensions are handled. The only provision, in this
rem implementation, is that they are of the same type and
rem have the same number and size of dimensions (except
rem string, in the last dimension).

1010 LOCal er

rem This nice little feature of "inheritance" is not
rem documented anywhere, as far as I know:
rem A LOCal variable defined in one procedure will remain
rem local in any procedure called by that procedure, unless
rem the variable has been "re-defined" by a subsequent use
rem of LOCal.
rem Here the local variable, er (error flag) is set in EQU,
rem the calling function, and modified in EQN/EQS. Yet a
rem variable er, defined in the initial code, outside any
rem procedure body, would retain its original value. The
rem only danger lies in that if the same sub-routines were
rem to be reused by another procedure, you may forget to
rem declare it as LOCal in the calling procedure and end up
rem mysteriously modifying a GLOBal variable instead!
rem It certainly saves the the repeated overhead of stacking
rem a local variable for each recursive call to EQN/EQS
rem here, as would be the case if we defined er both in EQU
rem and in EQN/EQS.

1020 IF PARTYP(a)<>PARTYP(b):RETurn -15
1030 IF PARUSE(a)<>PARUSE(b):RETurn -17

rem Checks whether paramerters are arrays, and of the same
rem 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

rem String arrays must be handled slightly differently to
rem numeric ones, in that the last dimension is the string
rem itself. It might be possible to find a universal
rem algorithm, to handle numbers and strings, but it makes
rem sense to use the built-in mass assignment features and
rem copy whole strings at once, rather than byte by byte. So
rem the string and numeric sides have been implented as
rem separate functions. Another advandage is that this
rem offers the opportunity to optimise them for their
rem different uses.

1100 END DEFine
1110 :
2000 DEFine FuNction  EQN(a,b)
2010 LOCal i%

rem Another reason for separating out these sub-routines as
rem functions, is that we can take advantage of the
rem interpreter's excellent array slicing abilities, as in
rem line 2070.

2020 IF DIMN(a)<>DIMN(b):RETurn -4

rem Every dimension has to match in size. This test will be
rem performed before any processing takes place. The
rem alternative would be to use a special sub-routine.

2030 IF DIMN(a(0))=0 THEN

rem If the next dimension is past the last, then this is the
rem dimension we can work with:

2040  FOR i%=0 TO DIMN(a):a(i%)=b(i%)

rem Copy this dimension from b to a, element by element.
rem 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%

rem More than anything, a function like EQU wants speed, so
rem loops have been specialised, reducing the overheads of
rem test & branch. The use of recursion is almost necessary,
rem but since most arrays will be of between 1 to 3
rem dimensions, and ditto levels of recursion, it shouldn't
rem significanly affect the routine's resource demands.

2090 END IF
2100 RETurn er

rem The error-checking stuff is not strictly necessary in a
rem programming toolkit - error-checking is performed at the
rem program level - but it's easier to delete it than add it
rem when needed (eg during program development).

2110 END DEFine
2120 :
3000 DEFine FuNction  EQS(a,b)

rem Pretty much the same as for EQN above, but optimised for
rem string operations.

3010 LOCal i%

rem Note that I can not use the same technique for i% as for
rem er. i%'s value will be different at different levels of
rem recursion ie, i% has to be saved between levels.

3020 IF DIMN(a)<>DIMN(b):RETurn -4

rem This arrangement was actually a bug, as no comparison is
rem made on the last dimension. However, as the interpreter
rem doesn't complain and simply ignores any supernumary
rem characters, I thought I'd leave it there as a "feature".
rem       Ie, DIM a$(5,10),b$(5,8): er=EQU(b$,a$)
rem works, though any strings longer than eight characters
rem will be truncated.

3030 IF DIMN(a(0,0))=0 THEN

rem Remember a() refers to an array of type string!
rem Operations can be performed at a higher structural
rem level, so we terminate recursion one level up.

3040  FOR i%=0 TO DIMN(a):a(i%)=b(i%)

rem 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 :

rem Genuine bug/incompatibility reports and comments welcome
rem Send to      pjwitte at googlemail dot c0m
rem <- - - - - - - - - - cut here - - - - - - - - - - -> mer