SML PROGRAMMING

Debugging

"It's not a bug, it's a feature!"

I can count the bug-free programs I've written on one hand. If you write a program of any consequence, no matter how much care you lavish on its construction, chances are it will contain errors, some of which may not manifest themselves for a long time after you've submitted your program to public scrutiny.

COMPSML and LINKSML, however, aid in spotting errors before the program is ever run for the first time. COMPSML can identify a number of problems during the compilation process, and will indicate the line in the routine file in which they occur. Some of the more common ones I encounter are as follows:

1) Syntax errors

   Warning: This is not an SML command
This message generally results from mistyping an SML command or by inadvertently placing an ampersand (&) before an ARC/INFO command.

   Warning: Too many parameters
This message usually indicates improper construction of an SML statement.

2) Undefined aliases

   Warning: Alias not defined TEMP
These errors commonly result from forgetting to add an &include directive at the beginning of a routine or by mistyping the alias. Another common error is to use delimiters when defining a named variable:

   &define [temp] 10 &var
which causes the following message:

   Alias name contains alias delimiters or blanks
In addition, any reference to [temp] in the routine will generate an undefined alias error.

3) Alias collision

   Warning: redefining existing alias.
This will happen when you define in a routine a temporary variable name which is already used by the include file. A far more dangerous error, not spotted by COMPSML, is variable collision (see below for further discussion), which can be caused by assigning more than one name to the same variable.

4) Improper closure

   &IF or &WHILE has no &END
This occurs most frequently in programs with complex nesting, i.e. &if within &if within &while, etc. Use of indenting in programs to indicate nested structures greatly aids in finding where that missing &end should be. (Another common error to watch out for is forgetting to increment the counter variable in a &while loop.)

A comprehensive discussion of COMPSML error messages is presented in Appendix A of the "SML User's Guide"; reading it gives a really good impression of what you can and can't do in a routine file.

If there are enough warnings to run off the screen, you can redirect the output to a file:

   COMPSML pspot r pspot >error.txt
LINKSML can also spot possible bugs by listing external routines, i.e. routine names that were not found by the linker. This would arise, for example, by mistyping the name in a &run statement.

Compiling to SML

One of the most frequent errors I've committed is not adding "%" to a named variable when it's necessary to do so. If at any time I'm unsure how named variables will be handled in the compilation process, I use COMPSML's "N" option:

   COMPSML test1 N
The "N" option causes each routine in a routine file to be processed into a separate SML file[1], revealing exactly how the SML processor will look at your program (also preserving &rem statements). Named variables will be translated to their numeric equivalents, and &if...&end and &while...&end directives will be translated into equivalent &goto and &goback statements. The SML files are fully executable, and on a fast computer are difficult to distinguish in run time from the final CML.

Note that because compiling to SML using COMPSML's "N" option also preserves blank lines, you will want to delete them in the resulting SML files anywhere in your application that the command processor will react to them, e.g. to terminate an action.

Let's imagine, for example, that I want to write an ARCEDIT routine to break a selected arc into segments. The first idea that comes to me is to use the SPLIT command, and after some thought I come up with the following:

   &routine breakup

   &define coo 11 &var
   &define arcno 12 &var
   &define numv 13 &var
   &define x 14 &var
   &define y 15 &var

   SHOW COORDINATE [coo]
   SHOW SELECT 1 [arcno]
   SHOW ARC [arcno] NPNTS [numv]
   COO KEY
   &while &ne [numv] 2 &do
      SHOW ARC [arcno] POINT 2 [x] [y]
      SPLIT
      1 [x] [y]
      SHOW SELECT 2 [arcno]
      RES $RECNO = [arcno]
      SHOW ARC [arcno] NPNTS [numv]
   &end
   COO [coo]
   &return
The underlying hypothesis, which I briefly tested in ARCEDIT before writing the code, is that if you split an arc at its second vertex, the remainder of the arc will be the second feature of the resulting selection set. Because I'm uncertain how the named variables will come out, I do a "COMPSML breakup N" to look at the resulting code:

   SHOW COORDINATE 11
   SHOW SELECT 1 12
   SHOW ARC 12 NPNTS 13
   COO KEY
    &LABEL *WHILE_000*
    &goto *END___000* &if &EQ %13 2
      SHOW ARC 12 POINT 2 14 15
      SPLIT
      1 %14 %15
      SHOW SELECT 2 12
      RES $RECNO = %12
      SHOW ARC 12 NPNTS 13
    &goback *WHILE_000*
    &LABEL *END___000*
   COO %11
   &return
Right away I see that variable 12 lacks a percent sign in the "SHOW ARC" commands, so I fix the source code by placing "%" in front of "[arcno]" in those statements.

Runtime Debugging

I run the program on a test arc and it works fine. Then I try another arc and things just puke up all over the place. What's going on? The initial error messages, of course, flew by too fast for me to see them. This is a job for &echo &debug. The &echo &debug directive allows you to step through a program until you reach the point where things start going wrong. When a program is running in debug mode, the line which is about to be executed appears along with a "DEBUG:" prompt. Pressing <Enter> executes that line. In the meantime, you can issue just about any command[2], such as:

[TIP: A modular program structure simplifies the job of working backwards to the original error. If you find that the routine which crashes isn't receiving correct variable values, you can move the &echo &debug statement to the previous routine, and so on, until you find out just where things are screwing up. Another technique to isolate nonfatal errors is to sprinkle the suspect routine(s) with some &lv and &key statements to list variable contents and pause the program while you're looking at them.]

To see, therefore, what's happening in the breakup routine, I add &echo &debug to the beginning of the SML, restart the ARCEDIT session (I had to use <Break> to get out of the loop I was stuck in), select the arc that was giving me trouble, and run the program. I step through the program until the first error message appears:

        SPLIT
  DEBUG:
  Point to where the arc should be
  split(From keyboard)
        %14, %15
  DEBUG:
  Cannot split arc on endpoints
        SHOW SELECT 2 12
  DEBUG:
Aha! Here's a situation I hadn't anticipated. Vertices 1 and 2 must be identical, a problem I could easily solve with some extra variables and lines of code to compare coordinates, skipping to the next vertex if they are identical. Let's just double check the variables for kicks:

   DEBUG:&lv 11 15
   VAR.  VALUE
   %0011 CURSOR
   %0012 9
   %0013 34
   %0014 262314.90600
   %0015 117457.80500
         SHOW SELECT 2 12
   DEBUG:
Looks ok. I issue a &stop command and use SHOW to verify that the two vertices are in fact identical:

   DEBUG:&stop
   : SHOW ARC 9 VERTEX 1
   262315.12500 117457.80500
   : SHOW ARC 9 VERTEX 2
   262314.90600 117457.80500
   :
Oh boy. Things are more complex than I thought. Looks like I'll need to compare distances between vertices and skip those under the WEED value. Perhaps the SPLIT approach isn't the best one to take. An alternative might be to read the vertex coordinates, build a GENERATE file, create a temporary coverage, turn SNAPT off, DELETE the original arc, GET the new arcs, and restore the SNAPT setting. I could actually break up multiple arcs that way. But then I'll need to worry about restoring any attributes....

It's not a job—it's a way of life.

Variable Collision

As mentioned earlier, variable collision can occur when routines use the same variable for different purposes. This bug can be difficult to spot, even in relatively simple applications:

   &routine pspot

   &rem *Argument variables*
   &define coverage 1 &var
   &define radius 2 &var
   &define outline 3 &var
   &rem *Housekeeping variables*
   &define arc 51 &var
   &define wksp 52 &var
   &define curdir 54 &var

   &value [arc] ARC
   &value [wksp] WKSP
   &run curdir [curdir]
What just happened? Answer: the program nuked its own [coverage] argument, replacing it with the value "54". Always save global arguments to other variables. In any case, the best approach is to take advantage of local variables and structured routine calls:

   &routine pspot

   &rem *Argument variables*
   &define coverage -1 &var
   &define radius -2 &var
   &define outline -3 &var
   &rem *Housekeeping variables*
   &define arc 51 &var
   &define wksp 52 &var
   &define curdir 54 &var

   &value [arc] ARC
   &value [wksp] WKSP
   &r curdir
   &rv [curdir]
Use the following rules of thumb for variable management:

Variables

Type

Use

-20 to -1 Local &r arguments and scratch variables
0 Dummy Assign to unwanted SHOW values
1 Memory WIN return value[3]
2-50 Memory Data maintained across routines
51 to 9999 Extended WIN dialogs, arrays, and data maintained across modules

Know Your Logic!

A good grasp of logic and program flow is essential in finding (not to mention avoiding) poorly formed statements. For example, I may want to respond to a situation where the following statement:

   &eq %1 101 &and &eq %11 1
is FALSE. If SML supported Boolean expressions I could just take the statement, enclose it in parentheses, and place a big &not in front. Lacking that option, I try what seems in my haste to be the correct logical inverse:

   &if &ne %1 101 &and &ne %11 1 &do
      &type "Invalid selection"
   &end
It doesn't work. Those of you sitting safely at home may recognize the error right away, but I don't. So I do a "COMPSML N" and look at the resulting code:

    &goto *FALSE_000* &if &EQ %1 101
    &goto *FALSE_000* &if &EQ %11 1
      &type "Invalid selection"
    &LABEL *FALSE_000*
Tracing the flow, I realize that it's just not right. If, for example, variable 1 contained "101" but variable 11 contained "2", the &type statement would still be bypassed. I'm puzzled. I'm inclined to blame the compiler. Then I look back at the original routine, recognize my error, and replace &and with &or, which yields the following result upon compilation:

    &goto *TRUE__000* &if &NE %1 101
    &goto *FALSE_000* &if &EQ %11 1
    &LABEL *TRUE__000*
      &type "Invalid selection"
    &LABEL *FALSE_000*
That's still not right! I don't want "Invalid selection" to appear if neither variable 1 contains "101" nor variable 11 contains "1". Since the logical inverse is correct, my original logic must be bad. What I'm really after is this:

   &if &eq %1 101 &xor &eq %11 1 &do
      &type "Invalid selection"
   &end
Since, however, SML lacks an &xor directive, I have to express the statement as follows:

   &if &eq %1 101 &or &eq %11 1 &do
      &if &ne %1 101 &or &ne %11 1 &do
         &type "Invalid selection"
      &end
   &end
which compiles into the following:

    &goto *TRUE__000* &if &EQ %1 101
    &goto *FALSE_000* &if &NE %11 1
    &LABEL *TRUE__000*
       &goto *TRUE__001* &if &NE %1 101
       &goto *FALSE_001* &if &EQ %11 1
       &LABEL *TRUE__001*
         &type "Invalid selection"
       &LABEL *FALSE_001*
    &LABEL *FALSE_000*

Error Control

There are two types of error control: prevention and trapping. Error prevention involves checking for improper situations before they snarl up a routine. For applications involving menus and dialog boxes, this may consist of graying out a command or widget so that it's unavailable to the user. Other situations, however, cannot be anticipated until you're well into the body of the routine. For example, in a "zoom select" routine, you won't know until after you collect coordinates that the minimum and maximum X values are the same (MAPEXTENT hates identical X or Y values).

Sometimes it's best to keep error prevention to a minimum and assume that the user pretty much knows what's going on. Otherwise, you can really go overboard with the extra padding and straps. An extreme case of error prevention in the breakup routine might be as follows:

   &run sysprogr [resp]
   &if &ne [resp] ARCEDIT &and &ne [resp]
   ARCEDITW &do
      &type "Must be run in ARCEDIT."
      &return
   &end
   SHOW NUMBER SELECT [numsel]
   &if &ne [numsel] 1 &do
      &type "Only one arc may be selected."
      &return
   &end
   SHOW SELECT 1 [arcno]
   SHOW ARC %[arcno] NPNTS [numv]
   &if &eq [numv] 2 &do
      &type "Arc must have more than two vertices."
      &return
   &end
Note that the check for number of vertices is unnecessary, for if [numv] equals 2 the &while loop will not execute anyway.

Error trapping prevents error propagation. In many cases, a routine will simply continue to the end without generating anything more serious than an error message or two, but if an SML command is involved the application could bomb. Or if another routine acts on erroneous results from a routine it called, a mess could result. Unfortunately, unlike AML, SML has no intrinsic commands for dealing with errors. Nonetheless, it can be desirable to monitor an action for failure, prevent further action, and inform the user of the situation.

SML routines can readily signal error conditions to each other using global variables:

   &routine pspot
   ...
   &r curdir [curdir]
   &if &ne "[error]" "OK" &do
      &r bailout
      &return
   &end
   ...
   &return

   &routine curdir
   &sv [error] OK
   ...
   &open [wksp]t$curdir ioerror
   ...
   &label ioerror
   &sv [error] "CURDIR: Could not open temporary file."
   &return
or via return values:

   &routine pspot
   ...
   &r curdir [curdir]
   &rv [error]
   &if &ne "[error]" "OK" &do
      &r bailout
      &return
   &end
   ...
   &return

   &routine curdir
   &sv [error] OK
   ...
   &open [wksp]t$curdir ioerror
   ...
   &return [error]
   &label ioerror
   &sv [error] "CURDIR: Could not open temporary file."
   &return [error]
The test of really good nested error trapping is the ability to pass error notices (or cancels, for that matter) back on up to the highest program level (or to the main menu).

You can't always communicate errors through variables. If you were to launch another application, you would have to rely either upon a file or clipboard contents to inform the host program of success or failure:

   WIN RUNW arcx.bat [wksp]p$run.sml
   WIN CB R
   &if &ne "%1" "OK" &do
      &value [error] 1
      &run bailout
      &return
   &end
Finally, any routine that aborts a complex process should clean up after itself:

1) Destroy any menus and/or dialog boxes.

2) Put up a message box explaining the error.

3) Find and delete as many temp files as possible without destroying essential system files (do NOT use "& DEL [wksp]t$*.*"!!).

4) Do not QUIT in case something can be salvaged from the session: issue &stop to bring the user to the command prompt.

Next: Menus


[1]If you do this more than once, be sure to delete the existing SML files or the process will abort. In cases where there are many routines to compile and I have any other SMLs present that I use for testing, I keep the test SMLs in a separate directory and use a batch file to delete *.SML, COMPSML xxxx N, and recopy the test SMLs into the current directory.

[2]You cannot use &goto, &run, etc. to jump to another location within a CML file—see the &echo entry of the Online Help for more information.

[3]The WIN command will be discussed under Windows Extensions.


Return to ArcTips page