Skip to content

A Creative Use of ⎕WATCHPOINTS to Investigate Large Heritage ws in APL+Win

Vector 19:1, page 92

development practice programming techniques

Transcribed from the printed issue; not yet reviewed. Read it in the PDF of the issue, page 92

Many commercially valuable APL workspaces consist of a nucleus of 10-20 year old code, possibly written originally to run on a mainframe and ported many times since between quite differently structured interactive environments, most recently to an event-driven environment such as Windows. The tool to be demonstrated comes into its own when you encounter such code for the first time and become responsible for maintaining it. Maybe the workspace has been built to engineering standards, with well-chosen naming conventions that have been rigorously adhered-to, consisting of several non-overlapping well-defined and smallish groups of functions, each of which constitutes a black-box and which can in principle be copied into a fresh workspace and studied in isolation with representative test data. Some hopes!

Such a workspace may contain over 1000 functions, each having between 10 and 500 lines of code, with perhaps 500 (semi)global variables being created and modified at runtime, few of which may be present when the workspace is first loaded. Functions have side-effects not described in their comments (which are not dependably up-to-date), they only work in-situ, bathed in a history-dependent environment, and extracting the closure of the set of functions calling/called by a given function ends up with roughly half the total number of functions. The workspace itself has no reliable cleavage lines which remain the same from function to function. It is however ‘lumpy’. Thus, two groups of names can be discerned, those all in uppercase such as XINTRCV2 and those in mixed case with names like fnProcIntContentsA or printMultiPageDoc. The former you surmise is the heritage stratum, the latter is (largely) the interface to Windows. But when it comes to subsequent extensions to the heritage nucleus, the programmer concerned chose names according to his or her own inclinations. Meaningful – to whom? Symbolic – so what’s the naming scheme? Compliant – sez who?

The question to be answered is this: when I perform such-and-such an interaction, what happens internally? To be precise: what functions are executed, and what globals do they alter? Traditional tools are of little use in such a code-‘soup’, until you basically understand how the workspace is structured. Only then can you guess which function to set a ⎕STOP in, and so begin a line-by-line walkthrough in an attempt to understand how the application works. Even then you may find that superficially quite straightforward interactions execute hundreds of lines in dozens of functions, most of which serve only to reassure the programmer that the ‘environment’ or ‘soup’ of cached data in the form of globals is up-to-date, at least where it matters to the task being performed.

Most newcomers to a team write exploratory tools for their own use. Once they become broadly familiar with how it all works the tool comes to be needed less and less. The owner even forgets how to use it, and so is less inclined to use it even when it would be useful. So the effort is largely once-off and soon written-off.

I wrote this tool with other people in mind (which meant me too, a few months down the road). I carefully avoid saying ‘written with ease-of-use in mind’. Every programmer writes with ease-of-use in mind. Just as every politician is on the side of good government and against corruption. More to the point, the development team anticipated further newcomers, I found the tool most useful in practice and so I felt I could justify the effort. Thinking of new tasks for it justified the effort of designing a robust, easy-to-extend structure. Years of suffering at the hands of other people’s tools which left sticky footprints in my workspaces made me design it as a single function, working in a way which any APLer could understand. Years of leaning over people’s shoulders and using their keyboards made me avoid delta and delta-underbar. It has no status globals, register entries or .INI entries. This tool is the APL counterpart of the Willys Jeep. You can maintain it with nothing but a screwdriver and a monkey wrench.

The function is: tkv (short for ‘track variables’). It creates four global variables in your workspace:

  • tkv_L – a list of variable names to be tracked.
  • tkv_T – a table of trace information which tkv builds and maintains. You don’t handle it directly: tkv will display it for you in a choice of formats.
  • tkv_I – assignment counter
  • tkv_TS – a snapshot of the timestamp ⎕TS, being the last time anything was added to tkv_T. It furnishes an identification of the display output, when you come to make a collection of these things.

I could have combined them into one nested global; I could have kept this cache of data elsewhere; I could have written it as a ‘user command’… I didn’t, because it traded simplicity and portability for little or no gain.

If by a remote chance the prefix: ‘tkv’ happens to cause a name clash in your workspace, you may select another prefix by replacing all instances of ‘tkv’ in the function code, including line [0].

Here’s how to use it. Assume the target workspace has been loaded and is waiting for an interaction. You have access to the APL session log and you have copied in fn: tkv

Now, turning to the APL session window, enter:

      tkv ⎕nl 2
>>> watchpoints inactive
>>> tracking table emptied, size=428
>>> these reserved vars removed from list tkv_L: tkv_L tkv_T tkv_I
>>> these too-frequently assigned vars removed from list tkv_L: ⍙ELX ⍙LOG t
>>> watchpoints active on 422 vars
>>> resume manual interaction with form(s)...

The right argument used here is a list of all the vars currently in the workspace. This is placed in the global: tkv_L. Alternatively you can load or copy tkv_L from any source you like, or create it by hand. Note that if tkv discovers it in char matrix form it converts it to a vector of nested char vectors (‘strings’). Or if you just want to use or re-use the existing tkv_L, provide an empty right argument like this:

      tkv''
>>> watchpoints inactive
>>> tracking table emptied, size=422
>>> watchpoints active on 422 vars
>>> resume manual interaction with form(s)...

Now perform the desired sample interaction with the application. Turning back to the APL session, to view the results enter:

      zi←tkv'i'
      )ed zi

This generates the following report as a char vector with embedded ⎕TCNL chars in a global variable, zi:

=========================================
GLOBAL TRACKING TABLE at:  4 Oct 2001 11:39
=========================================

>>> last callback was: 14
Common suffix="fmLex_Page_Toolbar[36]>"

PageNumber:        1:
PrintPageNum:      2: fmLex_ShowPage[21]
PrintPageMax:      3: fmLex_GetHead[33]fmLex_ShowHead[18]fmLex_ShowPage[25
]
WINFO:             4: fmLex_ShowHead[22]fmLex_ShowPage[25]
ROWattrMAT:        5: fmLex_View[32]fmLex_ShowHead[36]fmLex_ShowPage[25]
PrintPageMax:      6: fmLex_GetHead[33]fmLex_ShowHead[57]fmLex_ShowPage[25
]
WINFO:             7: fmLex_ShowHead[59]fmLex_ShowPage[25]
DATAsigns:         8: GENoutFSM[15]fmLex_ShowPage[28]
RPTattrMAT:        9: GENoutFSM[15]fmLex_ShowPage[28]
pagZERO:          10: fmLex_ShowPage[30]
rho:              11: SetGridAttr[76]fmLex_ShowPage[37]
WINFO:            12: fmLex_ShowNames[50]fmLex_ShowPage[42]
PrintHeader:      13: fmLex_ShowPage[57]
PrintFooter:      14: fmLex_ShowPage[58]

Each sequence-numbered entry in the option ‘I’ report contains an extract of ⎕SI as it was when the assignment was made. To reduce the bulk of the output, tkv recognizes if a repeated suffix is common to every entry and strips it. The text of the suffix is not lost, however, you can see it at the head of the report:

Common suffix="fmLex_Page_Toolbar[36]>"

You might hazard a guess that the whole transaction was triggered by pressing a toolbar button, for which the callback fn happened to be: fmLex_Page_Toolbar and all the work was done in line [36]. Notice that the data for the first assignment which took place (to var: PageNumber) contains nothing but the suffix, so the entry itself is empty.

The number in column 2 shows the order in which assignments occurred. A familiar feature of APL+ takes you directly to the line concerned when you double-click on column 3, say upon: fmLex_ShowHead[22] in the entry 7: for WINFO which happens to be:

      WINFO[1 2 3]←winfo[1 2 3]

Alternatively you might want to see the same data in the same order as your original list of variables, tkv_L. In the APL session, enter:

      zc←tkv'c'
      )ed zc

which generates the following report in zc:

=========================================
GLOBAL TRACKING TABLE at:  4 Oct 2001 11:39
=========================================

>>> last callback was: 14

DATAsigns
    8: GENoutFSM[15]fmLex_ShowPage[28]fmLex_Page_Toolbar[36]>
PageNumber
    1: fmLex_Page_Toolbar[36]>
PrintFooter
   14: fmLex_ShowPage[58]fmLex_Page_Toolbar[36]>
PrintHeader
   13: fmLex_ShowPage[57]fmLex_Page_Toolbar[36]>
PrintPageMax
    3: fmLex_GetHead[33]fmLex_ShowHead[18]fmLex_ShowPage[25]fmLex_Page_Toolbar[36]>
    6: fmLex_GetHead[33]fmLex_ShowHead[57]fmLex_ShowPage[25]fmLex_Page_Toolbar[36]>
PrintPageNum
    2: fmLex_ShowPage[21]fmLex_Page_Toolbar[36]>
ROWattrMAT
    5: fmLex_View[32]fmLex_ShowHead[36]fmLex_ShowPage[25]fmLex_Page_Toolbar[36]>
RPTattrMAT
    9: GENoutFSM[15]fmLex_ShowPage[28]fmLex_Page_Toolbar[36]>
WINFO
    4: fmLex_ShowHead[22]fmLex_ShowPage[25]fmLex_Page_Toolbar[36]>
    7: fmLex_ShowHead[59]fmLex_ShowPage[25]fmLex_Page_Toolbar[36]>
   12: fmLex_ShowNames[50]fmLex_ShowPage[42]fmLex_Page_Toolbar[36]>
pagZERO
   10: fmLex_ShowPage[30]fmLex_Page_Toolbar[36]>
rho
   11: SetGridAttr[76]fmLex_ShowPage[37]fmLex_Page_Toolbar[36]>

This table (option ‘C’) contains the same information as the previous one (option ‘I’), but sorted by global name instead of by the order of assignments.

If you want to see the whole tracking table, whether or not a given variable was assigned, then use option ‘S’ instead of ‘C’. Unlike options ‘C’ and ‘I’ which only show entries for variables actually assigned, this table has an entry for every tracked variable. With a large number of tracked variables, option ‘S’ isn’t quite so useful as option ‘C’ since most will be blind entries, like the first 45 below (not all shown – the list has 463 entries so it has been snipped):

=========================================
GLOBAL TRACKING TABLE at:  4 Oct 2001 11:39
=========================================

>>> last callback was: 14

ACCESS_MANAGER
ACCTnrs
ACTcalctype
ACTcommon
ACTdim
...
DATAdir
DATAnumber
DATAsigns
    8: GENoutFSM[15]fmLex_ShowPage[28]fmLex_Page_Toolbar[36]>
DATTN
DAYPER
DBFM
DBI
...

I must say I don’t use ‘S’ much. But it has a forensic value: if a variable isn’t listed then it was not in table tkv_L and so was not being tracked. If a variable is missing from the option ‘C’ or ‘I’ tables however, it might also be because it just wasn’t assigned.

What we see from all three lists (notice that their titles all carry the same timestamp, contained in tkv_TS) is that the sample interaction made 14 assignments to a total of 11 globals out of the 463 in the tracking list. We can see just this summary information by option ‘O’ (alias: ‘o’ or ‘0’):

      tkv'o'
>>> tracking table size=463
>>>  globals unassigned=452
>>>           " assigned=11
>>> last callback was: 14

The tool is meant for tracking globals only, although it can in principle track locals. It explicitly filters out assignments to locals, or more importantly localized versions of a given global, since such assignments confound the global being tracked. This feature (which inspects ⎕SINL) must be turned off (using a code switch) if you want to track all assignments to a given name, whether local, global or semiglobal. In practice this isn’t a very useful thing to do, since (say) z in one fn bears little or no relation to z in another. Besides which the z entry grows enormous and contains little or no useful information, unless you already know a lot about the workspace (like where z is used).

How does it work? By assigning to ⎕WATCHPOINTS a 2-column array of which the first column is tkv_L, the list of variables, and the second column is a list of calls to tkv itself, the call at row n having argument: n. Thus, in our example, the first two rows are:

]rr ⎕watchpoints[1;]
'ACCESS_MANAGER' 'tkv 1'
]rr ⎕watchpoints[2;]
'ACCTnrs' 'tkv 2'

Whenever the variable named in row n gets created or reassigned, tkv runs with argument n, appending a formatted extract of ⎕SI to the nth element of tkv_T, thus building up the tracking table as the code executes. Note that ⎕WATCHPOINTS does not trigger a callback when a watched var is erased, but does trigger one upon an assignment that does not alter the value, e.g:

      WINFO←⌽⌽WINFO

Function: tkv is to be found inside a component file called TKV.sf, downloadable from the APL2000 website (http://www.apl2000.com/). Move TKV.sf into the folder of (say) )LIB 1 and access it like so:

      ]ufile 1 tkv
Now using file 1 TKV
      ]uload tkv /r
1 object loaded

You can then see instructions using the conventional ]TKV ?, or like this:

      tkv'?'
>>> RECOMMENDED USAGE:
 tkv ⎕NL 2 ⍝--start tracking all vars present (or provide your list)

>>> now resume interacting with the application, then...
 tkv 'S'   ⍝--turn off tracking and see the results

>>> To start a fresh tracking run...
 tkv ''    ⍝--uses existing list of vars: tkv_L, else like: tkv ' '
 tkv ' '   ⍝--recreates list: tkv_L from all vars present
 tkv ']'   ⍝--]ULOADs list: tkv_L

>>> now do the target action, then...
 tkv 'S'   ⍝--see the result, sorted by var
 tkv 'C'   ⍝--like 'S', but unassigned vars omitted
 tkv 'I'   ⍝--the result, sorted by order of assignment

>>> If and as required...
 tkv '+'   ⍝--turns ON tracking
 tkv '-'   ⍝--turns OFF tracking
 tkv 'O'   ⍝--summarizes tracking table
 tkv 'T'   ⍝--empties tracking table

>>> tkv creates these globals (erase after use):
 tkv_I     ⍝--tracking counter: counts assignments to sequence them
 tkv_L     ⍝--list of vars tracked (cmx or strings)
 tkv_T     ⍝--assignments table of vars in tkv_L (strings)
 tkv_TS    ⍝--⎕TS at latest callback of tkv
 )ERASE tkv_I tkv_L tkv_T tkv_TS  tvk
z←tkv_larg tkv tkv_n;i;j;tkv_title;zz;⎕ELX;⎕IO
⍝track the appearance of global vars.
⍝∇ IAC 05Oct2001
⍝mydev
 ⎕ELX←'⎕DM'
 ⎕IO←1 ⍝--sometimes this has changed locally when tkv is called
 z←⍬
 tkv_title←''

⍝---------------------
⍝ RT ARG OPTIONS:
⍝ '?' ⍝--shows usage
⍝ '' ' ' ']' 'S' 'I' 'C' 'O' '-' '+' ...see :CASE '?' below.
⍝
⍝(These are for internal use only)...
⍝ 'L'       --(re)create the tracking var list
⍝ 'T'       --reset the tracking table
⍝ 'H'       --output table header
⍝ '⍳'       --callback count message
⍝ 'O'       --summarise tkv_T
⍝ '~'       --remove reserved names from tkv_L
⍝ '↑'       --check tkv_L converted from cmx to (strings)
⍝ n         --watchpoint callback, n ∊ ⍳⍴tkv_L
⍝---------------------

 :If 20<↑⍴⎕SI ⍝--runaway recursion safety net...
     0 0⍴ HALT ⎕stop 'tkv'
HALT: ⍝--halts here if )SI gone too deep
 :EndIf

⍝INTERNAL UTILITIES (LEFT ARG∊ 1 2 3)...
 :If 2=⎕NC'tkv_larg'
    ⍝ ...Rt arg: tkv_n is now some value to be processed
     :Select tkv_larg
     :Case 1 ⍝--delete trailing blanks
         z←(⌽∨\⌽z≠' ')/z←tkv_n
         →0
     :Case 2 ⍝--return common suffix
         z←⍬
         :For zz :In tkv_n
             :If 0<⍴zz
                 :If 0<⍴z
                     z←(⌽∧\⌽z=(-⍴z)↑zz)/z
                 :Else
                     z←zz
                 :EndIf
             :EndIf
         :EndFor
         z←(']'=↑z)↓z ⍝--discount a leading ']'
         →0
     :Case 3 ⍝--reformat timestamp: tkv_n to: dd Mmm yyyy hh:mm
         zz←tkv_n
         z←2 0⍕zz[3]
         z←z,' ',(12 3⍴'JanFebMarAprMayJunJulAugSepOctNovDec')[zz[2];]
         z←z,' ',⍕zz[1]
         z←z,' ',¯2↑⍕100+zz[4]
         z←z,':',¯2↑⍕100+zz[5]
         →0
     :Else ⍝-assume it's a title for reporting output
         tkv_title←,⍕tkv_larg ⍝...and don't exit...
     :Endselect
 :EndIf

⍝Check for complex rt arg...
 :If 2=⍴⍴tkv_n ⍝--place cmx as nested array in tkv_L, then start tracking
 :OrIf (1=⍴⍴tkv_n)∧(2=≡tkv_n) ⍝--place nested array in tkv_L, then start tracking
     :If 2=⍴⍴tkv_n ⋄ tkv_n←(⎕SPLIT tkv_n)~¨' ' ⋄ :EndIf
     tkv_L←tkv_n
     z←tkv''
     →0
 :ElseIf (1=⍴⍴tkv_n)∧(0≠×/⍴tkv_n) ⍝--treat as repeated calls
     z←∊tkv¨tkv_n
     →0
 :EndIf



 :Select tkv_n

 ⍝---------------------
 :Case '?' ⍝--show usage
 ⍝---------------------
     ">>> RECOMMENDED USAGE:"
     " tkv ⎕NL 2 ⍝--start tracking all vars (or provide your own list)"
     ""
     ">>> now resume interacting with the application, then..."
     " tkv 'S'   ⍝--turn off tracking and see the results"
     ""
     ">>> To start a fresh tracking run..."
     " tkv ''    ⍝--uses existing list of vars: tkv_L, else like: tkv ' '"
     " tkv ' '   ⍝--recreates list: tkv_L from all vars present"
     " tkv ']'   ⍝--]ULOADs list: tkv_L"
     ""
     ">>> now do the target action, then..."
     " tkv 'S'   ⍝--see the result, sorted by var"
     " tkv 'C'   ⍝--like 'S', but unassigned vars omitted"
     " tkv 'I'   ⍝--the result, sorted by order of assignment"
     ""
     ">>> If and as required..."
     " tkv '+'   ⍝--turns ON tracking"
     " tkv '-'   ⍝--turns OFF tracking"
     " tkv 'O'   ⍝--summarizes tracking table"
     " tkv 'T'   ⍝--empties tracking table"
     ""
     ">>> tkv creates these globals (erase after use):"
     " tkv_I     ⍝--tracking counter: counts assignments to sequence them"
     " tkv_L     ⍝--list of vars tracked (cmx or strings)"
     " tkv_T     ⍝--assignments table of vars in tkv_L (strings)"
     " tkv_TS    ⍝--⎕TS at latest callback of tkv"
     " )ERASE tkv_I tkv_L tkv_T tkv_TS  tkv"

 ⍝---------------------
 :CaseList ⍬ '' ' ' ']' ⍝--Inits, clears globs & runs OPENrtpg
 ⍝---------------------
     z←tkv'-'
    ⍝Decide what to do with (existing?) var table: tkv_L
     :If     tkv_n≡' ' ⋄ z←tkv'L'
     :ElseIf tkv_n≡']' ⋄ z←tkv'#'
     :EndIf

     z←tkv'T+'
     '>>> resume manual interaction with form(s)...'

     z←0 0⍴0

 ⍝---------------------
 :Case 'L' ⍝--regenerate the tracking list tkv_L from all vars found in ws
 ⍝---------------------
     tkv_L←((⎕SPLIT ⎕NL 2)~¨' ')
     z←tkv'~'

     z←0 0⍴0

 ⍝---------------------
 :Case '#' ⍝--load the tracking list tkv_L
 ⍝---------------------
     :If 2=⎕NC'tkv_L' ⋄ zz←tkv_L ⋄ :Else ⋄ zz←0 ⋄ :EndIf
     ⎕UCMD'ULOAD tkv_L /REPLACE'

    ⍝How has tkv_L changed, if at all?
     :If 2=⎕NC'tkv_L'
         :If zz≡tkv_L
             '>>> var list found, size=',⍕⍴tkv_L
         :Else
             '>>> new var list loaded, size=',⍕⍴tkv_L
         :EndIf
     :Else
         z←tkv'L'
         '>>> no tracking list found, list regenerated, size=',⍕⍴tkv_L
     :EndIf

     z←0 0⍴0

 ⍝---------------------
 :Case 'H' ⍝--table header
 ⍝---------------------
     z←'GLOBAL TRACKING TABLE at: ',3 tkv tkv_TS
     z←z,(0≠×/⍴tkv_title)/⎕TCNL,tkv_title
     i←(⍴z)⍴'=' ⋄ z←i,⎕TCNL,z,⎕TCNL,i,⎕TCNL
     z←z,tkv'⍳'

 ⍝---------------------
 :Case '⍳' ⍝--callback count
 ⍝---------------------
     z←⎕TCNL,'>>> last callback was: ',(⍕tkv_I),⎕TCNL

 ⍝---------------------
 :CaseList 's' 'S' '⎕' ⍝--show the table, sort by tkv_T
 ⍝---------------------
     →(0=tkv_I)/ERR0
     z←tkv'-' ⍝--turn off tracking
     z←tkv_title tkv'H'
     :For i :In ⍳↑⍴tkv_T
         z←z,⎕TCNL,⍕(i⊃tkv_L),(i⊃tkv_T)
     :EndFor

 ⍝---------------------
 :CaseList 'c' 'C' ⍝--show the table, sort by tkv_T, omitting blind entries
 ⍝---------------------
     →(0=tkv_I)/ERR0
     z←tkv'-' ⍝--turn off tracking
     z←tkv_title tkv'H'
     :For i :In ⍳↑⍴tkv_T
         :If 0<⍴i⊃tkv_T
             z←z,⎕TCNL,⍕(i⊃tkv_L),(i⊃tkv_T)
         :EndIf
     :EndFor

 ⍝---------------------
 :CaseList 'i' 'I' '⍞' ⍝--show the table, sort by tkv_I
 ⍝---------------------
     →(0=tkv_I)/ERR0
     z←tkv'-' ⍝--turn off tracking
     z←⍬
     j←⊃⊃⌈/⍴¨tkv_L ⍝--width of global name field
     :For i :In ⍳↑⍴tkv_T
         :If 0<×/⍴zz←i⊃tkv_T
             zz←(zz≠⎕TCNL)⊂zz
             zz←zz,¨⊂j↑(i⊃tkv_L),':'
             z←z,zz
         :EndIf
     :EndFor
     zz←⍎¨5↑¨z ⍝--counter vec (NB: change 5? Change 5 in line [206] too)
     z←z[⍋zz] ⍝--sort zz by counter field
     z←1 tkv¨(-j)⌽¨z ⍝--bring global name field to front & trim
     z←(-↑⍴zz←2 tkv z)↓¨z ⍝--drop common suffix zz from z
     z←(tkv_title tkv'H'),('Common suffix="',zz,'"',⎕TCNL),∊⎕TCNL,¨z

 ⍝---------------------
 :Case '~' ⍝--remove tkv locals & globals from the tracking list tkv_L
 ⍝---------------------
     z←'zz' 'tkv_larg' 'tkv_n' 'tkv_L' 'tkv_T' 'tkv_I' 'tkv_TS' 'tkv_title',,¨'ijz'
     :If 0≠⍴z←(z∊tkv_L)/z
         tkv_L←tkv_L~z
         '>>> these reserved vars removed from list tkv_L:',⍕z
     :EndIf

     :IF 1 ⍝--CODE SWITCH: set to 0 to omit removal of the following vars...
         z←'⍙ELX' '⍙LOG',,¨'cdrt'
         :If 0≠⍴z←(z∊tkv_L)/z
             tkv_L←tkv_L~z
             '>>> these too-frequently assigned vars removed from tkv_L:',⍕z
         :EndIf
     :ENDIF

 ⍝---------------------
 :Case '↑' ⍝--ensure tkv_L is (strings) not cmx
 ⍝---------------------
     :If 2=⍴⍴tkv_L
         tkv_L←(⎕SPLIT tkv_L)~¨' '
     :EndIf

     z←0 0⍴0

 ⍝---------------------
 :CaseList 'O' 'o' '0' ⍝--summarise the tracking table
 ⍝---------------------
     :If (⍴tkv_L)≠⍴tkv_T
         z←'>>> tracking table mismatch: ',⍕(⍴tkv_L)(⍴tkv_T)
     :Else
         z←tkv'⍴'
         z←z,⎕TCNL,'>>>  globals unassigned=',⍕+/0=∊⍴¨tkv_T
         z←z,⎕TCNL,'>>>           " assigned=',⍕+/0≠∊⍴¨tkv_T
         z←z,tkv'⍳'
     :EndIf

 ⍝---------------------
 :Case '⍴' ⍝--tracking table size
 ⍝---------------------
     z←'>>> tracking table size=',⍕⍴tkv_T

 ⍝---------------------
 :Case 'T' ⍝--reset the tracking table
 ⍝---------------------
     tkv_T←(⍴tkv_L)⍴⊂⍬ ⍝--create empty tracking table
     tkv_I←0 ⍝--start the tracking counter
     '>>> tracking table emptied, size=',⍕⍴tkv_T

     z←0 0⍴0

 ⍝---------------------
 :Case '-' ⍝--turn watching OFF
 ⍝---------------------
     ⎕WATCHPOINTS←''
     '>>> watchpoints inactive'

     z←0 0⍴0

 ⍝---------------------
 :Case '+' ⍝--turn watching ON
 ⍝---------------------
     z←tkv'↑~'
     ⎕WATCHPOINTS←⍉tkv_L,[.5] (⊂'tkv '),¨⍕¨⍳⍴tkv_L
     '>>> watchpoints active on ',(⍕⍴tkv_L),' vars'

     z←0 0⍴0

 ⍝---------------------
 :CaseList ⍳⍴tkv_T ⍝--all the valid callbacks
 ⍝---------------------
     :If ~(⊂,tkv_n⊃tkv_L)∊(z≠' ')⊂z←,' ',⎕SINL ⍝--if var is not local...
    ⍝:If 1 ⍝--whether var is localised or not...
    ⍝CODE SWITCH: ...decomment one or other of the above :If-statements
         z←tkv_n⊃tkv_L                ⍝ the var name
        ⍝z,' ← ',(⎕UCMD'RR ',z)      ⍝ trace the assignment in Session Window
    ⍝CODE SWITCH: ...decomment the above line to trace in Session Window
    ⍝                ...this needs the ucmd: ]RR to be installed
         z←tkv_n⊃tkv_T                ⍝ get the tracking table entry
         z←z,⎕TCNL                    ⍝ callback entry separator
         z←z,5 0⍕tkv_I←tkv_I+1        ⍝ callback counter prefixed
         z←z,': '                     ⍝ callback counter terminator
         i←(,2 0↓⎕SI)~' '             ⍝ omit ⎕SI[1 2;] (tkv[.] * ⎕WATCHPOINT[1])
         i←((i∊'<>')⍳1)↑i             ⍝ strip redundant on-event & event info
         z←z,i                        ⍝ append the list of fns
         (tkv_n⊃tkv_T)←z              ⍝ ...put z back into table
         tkv_TS←⎕TS                   ⍝ record the time
     :EndIf

     z←0 ⍝--⎕WATCHPOINT _needs_ the callback fn (tkv) to return 0
         ⍝  otherwise it suspends with VALUE ERROR !!

 ⍝---------------------
 :Else
 ⍝---------------------
     z←'>>> bad arg: ',⍕tkv_n

 :Endselect
 →0

ERR0: '>>> no globals were assigned' ⋄ →0