Skip to main content

Command-Line Routine Debugging

This topic describes techniques for testing and debugging Object Script code. InterSystems IRIS® data platform gives you two ways to debug code:

  • Use the BREAK command in routine code to suspend execution and allow you to examine what is happening.

  • Use the ZBREAK command to invoke the ObjectScript Debugger to interrupt execution and allow you to examine both code and variables.

InterSystems IRIS includes the ability to suspend a routine and enter a shell that supports full debugging capabilities, as described in this topic. InterSystems IRIS also includes a secure debug shell, which has the advantage of ensuring that users are prevented from exceeding or circumventing their assigned privileges.

Secure Debug Shell

The secure debug shell helps better control access to sensitive data. It is an environment that allows users to perform basic debugging, such as stepping and displaying variables, but does not allow them to do anything that changes the execution path or results of a routine. This protects against access that can lead to issues such as manipulation, malicious role escalation, and the injection of code to run with higher privileges.

By default, users at the debug prompt maintain their current level of privileges. To enable the secure shell for the debug prompt and thereby restrict the commands that the user may issue, you must enable the secure debug shell for that user.

If enabled for the current user, the secure debug shell starts when a BREAK command is executed, a breakpoint or watchpoint is encountered, or an uncaught error is issued.

Within the secure debug shell, the user cannot invoke:

  • Any command that can modify a variable.

  • Any function that can modify a variable.

  • Any command that can call other routines.

  • Any command that affects the flow of the routine or the environment.

Within the secure debug shell, when a user attempts to invoke a restricted command or function, InterSystems IRIS throws a <COMMAND> or <FUNCTION> error, respectively.

Restricted Commands and Functions

This section lists the restricted activities within the secure debug shell:

Restricted ObjectScript Commands

The following are the restricted ObjectScript commands for the secure debug shell:

  • CLOSE

  • DO

  • FOR

  • GOTO with an argument

  • KILL

  • LOCK

  • MERGE

  • OPEN

  • QUIT

  • READ

  • RETURN

  • SET

  • TCOMMIT

  • TROLLBACK

  • TSTART

  • VIEW

  • XECUTE

  • ZINSERT

  • ZKILL

  • ZREMOVE

  • ZSAVE

  • user commands except ZW and ZZDUMP

Restricted ObjectScript Functions

The following are the restricted ObjectScript functions for the secure debug shell:

  • $CLASSMETHOD

  • $COMPILE

  • $DATA(,var) — two-argument version only

  • $INCREMENT

  • $METHOD

  • $ORDER(,,var) — three-argument version only

  • $PROPERTY

  • $QUERY(,,var) — three-argument version only

  • $XECUTE

  • $ZF

  • $ZSEEK

  • any extrinsic function

Restricted Object Constructions

No method or property references are allowed. Property references are restricted because they could invoke a propertyGet method. Some examples of the object method and property syntax constructions that are restricted are:

  • #class(classname).ClassMethod()

  • oref.Method()

  • oref.Property

  • $SYSTEM.Class.Method()

  • ..Method()

  • ..Property

Note:

Even without passing a variable by reference, a method can modify public variables. Since a property reference could invoke a propGet method, no property access is allowed.

Debugging with the ObjectScript Debugger

The ObjectScript Debugger lets you test routines by inserting debugging commands directly into your routine code. Then, when you run the code, you can issue commands to test the conditions and the flow of processing within your application. Its major capabilities are:

  • Set breakpoints with the ZBREAK command at code locations and take specified actions when those points are reached.

  • Set watchpoints on local variables and take specified actions when the values of those variables change.

  • Interact with InterSystems IRIS during a breakpoint/watchpoint in a separate window.

  • Trace execution and output a trace record (to a terminal or other device) whenever the path of execution changes.

  • Display the execution stack.

  • Run an application on one device while debugging I/O goes to a second device. This enables full screen InterSystems IRIS applications to be debugged without disturbing the application’s terminal I/O.

Using Breakpoints and Watchpoints

The ObjectScript Debugger provides two ways to interrupt program execution:

  • Breakpoints

  • Watchpoints

A breakpoint is a location in an InterSystems IRIS routine that you specify with the ZBREAK command. When routine execution reaches that line, InterSystems IRIS suspends execution of the routine and, optionally, executes debugging actions you define. You can set breakpoints in up to 20 routines. You can set a maximum of 20 breakpoints within a particular routine.

A watchpoint is a variable you identify in a ZBREAK command. When its value is changed with a SET or KILL command, you can cause the interruption of routine execution and/or the execution of debugging actions you define within the ZBREAK command. Note that you cannot watchpoints for system variables.

Breakpoints and watchpoints you define are not maintained from one session to another. Therefore, you may find it useful to store breakpoint/watchpoint definitions in a routine or XECUTE command string so it is easy to reinstate them between sessions.

Establishing Breakpoints and Watchpoints

You use the ZBREAK command to establish breakpoints and watchpoints.

Syntax

ZBREAK location[:action:condition:execute_code]

where:

Argument Description
location Required. Specifies a code location (that sets a breakpoint) or local or system variable (which sets a watchpoint). If the location specified already has a breakpoint/watchpoint defined, the new specification completely replaces the old one. Note that you cannot watchpoints for system variables.
action Optional — Specifies the action to take when the breakpoint/watchpoint is triggered. For breakpoints, the action occurs before the line of code is executed. For watchpoints, the action occurs after the command that modifies the local variable. Actions may be upper- or lowercase, but must be enclosed in quotation marks.
condition

Optional — A boolean expression, enclosed in curly braces or quotes, that is evaluated when the breakpoint/watchpoint is triggered.

  • When condition is true (1), the action is carried out.

  • When condition is false, the action is not carried out and the code in execute_code is not executed.

If condition is not specified, the default is true.

execute_code Optional — Specifies ObjectScript code to be executed if condition is true. If the code is a literal, it must be surrounded by curly braces or quotation marks. This code is executed before the action being carried out. Before the code is executed, the value of the $TEST special system variable is saved. After the code has executed, the value of $TEST as it existed in the program being debugged is restored.
Note:

Using ZBREAK with a ? (question mark) displays help.

Setting Breakpoints with Code Locations

You specify code locations as a routine line reference that you can use in a call to the $TEXT function. A breakpoint occurs whenever execution reaches this point in the code, before the execution of the line of code. If you do not specify a routine name, InterSystems IRIS assumes the reference is to the current routine.

Argumentless GOTO in Breakpoint Execution Code

An argumentless GOTO is allowed in breakpoint execution code. Its effect is equivalent to executing an argumentless GOTO at the debugger BREAK prompt and execution proceeds until the next breakpoint.

For example, if the routine you are testing is in the current namespace, you can enter location values such as these:

Value Break Location
label^rou Break before the line at the line label label in the routine rou.
label+3^rou Break before the third line after the line label label in routine rou.
+3^rou Break before the third line in routine rou.

If the routine you are testing is currently loaded in memory (that is, an implicit or explicit ZLOAD was performed), you can use location values such as these:

Value Break Location
label Break before the line label at label.
label+3 Break before the third line after label.
+3 Break before the third line.

Setting Watchpoints with Local and System Variable Names

Local variable names cause a watchpoint to occur in these situations:

  • When the local variable is created

  • When a SET command changes the value of the local variable

  • When a KILL command deletes the local variable

Variable names are preceded by an asterisk, as in *a.

If you specify an array-variable name, the ObjectScript Debugger watches all descendant nodes. For instance, if you establish a watchpoint for array a, a change to a(5) or a(5,1) triggers the watchpoint.

The variable need not exist when you establish the watchpoint.

You can also use the following special system variables:

System Variable Trigger Event
$ZERROR Triggered whenever an error occurs, before invoking the error trap.
$ZTRAP Triggered whenever an error trap is set or cleared.
$IO Triggered whenever explicitly SET.

Action Argument Values

The following table describes the values you can use for the ZBREAK action argument.

Argument Description
"B" Default, except if you include the "T" action, then you must also explicitly include the "B" action, as in ZBREAK *a:"TB", to actually cause a break. Suspends execution and displays the line at which the break occurred along with a caret (^) indicating the point in the line. Then displays the Terminal prompt and allows interaction. Execution resumes with an argumentless GOTO command.
"L" Same as "B", except GOTO initiates single-step execution, stopping at the beginning of each line. When a DO command, user-defined function, or XECUTE command is encountered, single-step mode is suspended until that command or function completes.
"L+" Same as "B", except GOTO initiates single-step execution, stopping at the beginning of each line. DO commands, user-defined functions, and XECUTE commands do not suspend single-step mode.
"S" Same as "B", except GOTO initiates single-step execution, stopping at the beginning of each command. When a DO command, user-defined function, FOR command, or XECUTE command is encountered, single-step mode is suspended until that command or function completes.
"S+" Same as "B", except GOTO initiates single-step execution, stopping at the beginning of each command. DO commands, user-defined functions, FOR commands, and XECUTE commands do not suspend single-step mode.
"T" Can be used together with any other argument. Outputs a trace message to the trace device. This argument works only after you have set tracing to be ON with the ZBREAK /TRACE:ON command, described later. The trace device is the principal device unless you define it differently in the ZBREAK /TRACE command. If you use this argument with a breakpoint, you see the following message: TRACE: ZBREAK at label2^rou2. If you use this argument with a watchpoint, you see a trace message that names the variable being watched and the command being acted upon. In the example below, the variable a was being watched. It changed at the line test+1 in the routine test. TRACE: ZBREAK SET a=2 at test+1^test. If you include the "T" action, you must also explicitly include the "B" action as in ZBREAK *a:"TB", to have an actual break occur.
"N" Take no action at this breakpoint/watchpoint. The condition expression is always evaluated and determines if the execute_code is executed.

ZBREAK Examples

The following example establishes a watchpoint that suspends execution whenever the local variable a is killed. No action is specified, so "B" is assumed.

ZBREAK *a::"'$DATA(a)"

The following example illustrates the above watchpoint acting on a direct mode ObjectScript command (rather than on a command issued from within a routine). The caret (^) points to the command that caused execution to be suspended:

USER>KILL a
KILL a
^
<BREAK>
USER 1s0>

The following example establishes a breakpoint that suspends execution and sets single-step mode at the beginning of the line label2^rou.

ZBREAK label2^rou:"L"

The following example shows how the break would appear when the routine is run. The caret (^) indicates where execution was suspended.

USER>DO ^rou
label2 SET x=1
 ^ 
<BREAK>label2^rou
USER 2d0>

In the following example, a breakpoint at line label3^rou does not suspend execution, because of the "N" action. However, if x<1 when the line label3^rou is reached, then flag is SET to x.

ZBREAK label3^rou:"N":"x<1":"SET flag=x"

The following example establishes a watchpoint that executes the code in ^GLO whenever the value of a changes. The double colon indicates no condition argument.

ZBREAK *a:"N"::"XECUTE ^GLO"

The following example establishes a watchpoint that causes a trace message to display whenever the value of b changes. The trace message will display only if trace mode has been turned on with the ZBREAK /TRACE:ON command.

ZBREAK *b:"T"

The following example establishes a watchpoint that suspends execution in single-step mode when variable a is set to 5.

ZBREAK *a:"S":"a=5"

When the break occurs in the following example, a caret (^) symbol points to the command that caused the variable a to be set to 5.

USER>DO ^test
FOR i=1:1:6 SET a=a+1
 ^ 
<BREAK>
test+3^test 
USER 3f0>WRITE a 
5

Disabling Breakpoints and Watchpoints

You can disable either:

  • Specific breakpoints and watchpoints

  • All breakpoints or watchpoints

Disabling Specific Breakpoints and Watchpoints

You can disable a breakpoint or watchpoint by preceding the location with a minus sign. The following command disables a breakpoint previously specified for location label2^rou:

ZBREAK -label2^rou

A disabled breakpoint is “turned off”, but InterSystems IRIS remembers its definition. You can enable the disabled breakpoint by preceding the location with a plus sign. The following command enables the previously disabled breakpoint:

ZBREAK +label2^rou

Disabling All Breakpoints and Watchpoints

You can disable all breakpoints or watchpoints by using the plus or minus signs without a location:

Sign Description
ZBREAK - Disable all defined breakpoints and watchpoints.
ZBREAK + Enable all defined breakpoints and watchpoint.

Delaying Execution of Breakpoints and Watchpoints

You can also delay the execution of a break/watchpoint for a specified number of iterations. You might have a line of code that appears within a loop that you want to break on periodically, rather than every time it is executed. To do so, establish the breakpoint as you would normally, then disable with a count following the location argument.

The following ZBREAK command causes the breakpoint at label2^rou to be disabled for 100 iterations. On the 101st time this line is executed, the specified breakpoint action occurs.

 ZBREAK label2^rou       ; establish the breakpoint 
 ZBREAK -label2^rou#100  ; disable it for 100 iterations 

Important:

A delayed breakpoint is not decremented when a line is repeatedly executed because it contains a FOR command.

Deleting Breakpoints and Watchpoints

You can delete individual break/watchpoints by preceding the location with a double minus sign; for example:

ZBREAK --label2^rou

After you have deleted a breakpoint/watchpoint, you can only reset it by defining it again.

To delete all breakpoints, issue the command:

ZBREAK /CLEAR

This command is performed automatically when an InterSystems IRIS process halts.

Single-step Breakpoint Actions

You can use single step execution to stop execution at the beginning of each line or of each command in your code. You can establish a single step breakpoint to specify actions and execution code to be executed at each step. Use the following syntax to define a single step breakpoint:

ZBREAK $:action[:condition:execute_code]

Unlike other breakpoints, ZBREAK $ does not cause a break, because breaks occur automatically as you single-step. ZBREAK $ lets you specify actions and execute code at each point where the debugger breaks as you step through the routine. It is especially useful in tracing executed lines or commands. For example, to trace executed lines in the application ^TEST:

USER>ZBREAK /TRACE:ON
USER>BREAK "L+"
USER>ZBREAK $:"T"

The "T" action specified alone (that is, without any other action code) suppresses the single step break that normally occurs automatically. (You can also suppress the single-step break by specifying the "N" action code — either with or without any other action codes.)

Establish the following single-step breakpoint definition if both tracing and breaking should occur:

USER>ZBREAK $:"TB"

Tracing Execution

You can control whether or not the "T" action of the ZBREAK command is enabled by using the following form of ZBREAK:

ZBREAK /TRACE:state[:device]

where state can be:

State Description
ON Enables tracing.
OFF Disables tracing.
ALL Enables tracing of application by performing the equivalent of: ZBREAK /TRACE:ON[:device] BREAK "L+" ZBREAK $:"T"

When device is used with the ALL or ON state keywords, trace messages are redirected to the specified device rather than to the principal device. If the device is not already open, InterSystems IRIS attempts to open it as a sequential file with WRITE and APPEND options.

When device is specified with the OFF state keyword, InterSystems IRIS closes the file if it is currently open.

Note:

ZBREAK /TRACE:OFF does not delete or disable the single-step breakpoint definition set up by ZBREAK /TRACE:ALL, nor does it clear the L+ single stepping set up by ZBREAK /TRACE:ALL. You must also issue the commands ZBREAK --$ and BREAK "C" to remove the single stepping; alternatively, you can use the single command BREAK "OFF" to turn off all debugging for the process.

Tracing messages are generated at breakpoints associated with a T action. With one exception, the trace message format is as follows for all breakpoints:

Trace: ZBREAK at line_reference

where line_reference is the line reference of the breakpoint.

The trace message format is slightly different for single step breakpoints when stepping is done by command:

Trace: ZBREAK at line_reference source_offset

where line_reference is the line reference of the breakpoint and source_offset is the 0-based offset to the location in the source line where the break has occurred.

Operating System Notes:

  • Windows — Trace messages to another device are supported on Windows platforms for terminal devices connected to a COM port, such as COM1:. You cannot use the console or a terminal window. You can specify a sequential file for the trace device

  • UNIX® — To send trace messages to another device on UNIX® platforms:

    1. Log in to /dev/tty01.

    2. Verify the device name by entering the tty command:

      $ tty
      /dev/tty01
      
    3. Issue the following command to avoid contention for the device:

      $ exec sleep 50000
      
    4. Return to your working window.

    5. Start and enter InterSystems IRIS.

    6. Issue your trace command:

      ZBREAK /T:ON:"/dev/tty01"
      
    7. Run your program.

      If you have set breakpoints or watchpoints with the T action, you see trace messages appear in the window connected to /dev/tty01.

Trace Message Format

If you set a code breakpoint, the following message appears:

Trace: ZBREAK at label2^rou2

If you set a variable watchpoint, one of the following messages appears:

Trace: ZBREAK SET var=val at label2^rou2 
Trace: ZBREAK SET var=Array Val at label2^rou2
Trace: ZBREAK KILL var at label2^rou2

  • var is the variable being watched.

  • val is the new value being set for that variable.

If you issue a NEW command, you receive no trace message. However, the trace on the variable is triggered the next time you issue a SET or KILL on the variable at the NEW level. If a variable is passed by reference to a routine, then that variable is still traced, even though the name has effectively changed.

INTERRUPT Keypress and Break

Normally, pressing the interrupt key sequence (typically CTRL-C) generates a trapable (<INTERRUPT>) error. To set interrupt processing to cause a break instead of an <INTERRUPT> error, use the following ZBREAK command: ZBREAK /INTERRUPT:Break

This causes a break to occur when you press the INTERRUPT key even if you have disabled interrupts at the application level for the device.

If you press the INTERRUPT key during a read from the terminal, you may have to press RETURN to display the break-mode prompt. To reset interrupt processing to generate an error rather than cause a break, issue the following command: ZBREAK /INTERRUPT:NORMAL

Displaying Information About the Current Debug Environment

To display information about the current debug environment, including all currently defined break or watchpoints, issue the ZBREAK command with no arguments.

The argumentless ZBREAK command describes the following aspects of the debug environment:

  • Whether CTRL-C causes a break

  • Whether trace output specified with the "T" action in the ZBREAK command displays

  • The location of all defined breakpoints, with flags describing their enabled/disabled status, action, condition and executable code

  • All variables for which there are watchpoints, with flags describing their enabled/disabled status, action, condition and executable code

Output from this command is displayed on the device you have defined as your debug device, which is your principal device unless you have defined the debug device differently with the ZBREAK /DEBUG command described in the Using the Debug Device section.

The following table describes the flags provided for each breakpoint and watchpoint:

Display Section Meaning
Identification of break/watchpoint Line in routine for breakpoint. Local variable for watchpoint.
F: Flag providing information about the type of action defined in the ZBREAK command.
S: Number of iterations to delay execution of a breakpoint/watchpoint defined in a ZBREAK - command.
C: Condition argument set in ZBREAK command.
E: Execute_code argument set in ZBREAK command.

The following table describes how to interpret the F: value in a breakpoint/watchpoint display. The F: value is a list of the applicable values in the first column.

Value Meaning
E Breakpoint or watchpoint enabled
D Breakpoint or watchpoint disabled
B Perform a break
L Perform an "L"
L+ Perform an "L+"
S Perform an "S"
S+ Perform an "S+"
T Output a Trace Message

Default Display

When you first enter InterSystems IRIS and use ZB, the output is as follows:

USER>ZBREAK
BREAK: 
No breakpoints
No watchpoints

This means:

  • Trace execution is OFF

  • There is no break if CTRL-C is pressed

  • No break/watchpoints are defined

Display When Breakpoints and Watchpoints Exist

This example shows two breakpoints and one watchpoint being defined:

USER>ZBREAK +3^test:::{WRITE "IN test"}
USER>ZBREAK -+3^test#5
USER>ZBREAK +5^test:"L"
USER>ZBREAK -+5^test
USER>ZBREAK *a:"T":"a=5"
USER>ZBREAK /TRACE:ON
USER>ZBREAK
BREAK: TRACE ON
+3^test F:EB S:5 C: E:"WRITE ""IN test"""
+5^test F:DL S:0 C: E:
a F:ET S:0 C:"a=5" E:

The first two ZBREAK commands define a delayed breakpoint; the second two ZBREAK commands define a disabled breakpoint; the fifth ZBREAK command defines a watchpoint. The sixth ZBREAK command enables trace execution. The final ZBREAK command, with no arguments, displays information about current debug settings.

In the example, the ZBREAK display shows that:

  • Tracing is ON

  • There is no break if CTRL-C is pressed.

The output then describes the two breakpoints and one watchpoint:

  • The F flag for the first breakpoint equals EB and the S flag equals 5, which means that a breakpoint will occur the fifth time the line is encountered. The E flag displays executable code, which will run before the Terminal prompt for the break is displayed.

  • The F flag for the second breakpoint equals DL, which means it is disabled, but if enabled will break and then single-step through each line of code following the breakpoint location.

  • The F flag for the watchpoint is ET, which means the watchpoint is enabled. Since trace execution is ON, trace messages will appear on the trace device. Because no trace device was defined, the trace device will be the principal device.

  • The C flag means that trace is displayed only when condition is true.

Using the Debug Device

The debug device is the device where:

  • The ZBREAK command displays information about the debug environment.

  • The Terminal prompt appears if a break occurs.

Note:

On Windows platforms, trace messages to another device are supported only for terminal devices connected to a COM port, such as COM1:

When you enter InterSystems IRIS, the debug device will automatically be set to your principal device. At any time, debugging I/O can be sent to an alternate device with the command: ZBREAK /DEBUG:"device".

Note:

There are also operating-system-specific actions that you can take.

On UNIX® systems, to cause the break to occur on the tty01 device, issue the following command:

ZBREAK /D:"/dev/tty01/"

When a break occurs, because of a CTRL-C or to a breakpoint or watchpoint being triggered, it appears in the window connected to the device. That window becomes the active window.

If the device is not already open, an automatic OPEN is performed. If the device is already open, any existing OPEN parameters are respected.

Important:

If the device you specify is not an interactive device (such as a terminal), you may not be able to return from a break. However, the system does not enforce this restriction.

ObjectScript Debugger Example

First, suppose you are debugging the simple program named test shown below. The goal is to put 1 in variable a, 2 in variable b, and 3 in variable c.

test; Assign the values 1, 2, and 3 to the variables a, b, and c
 SET a=1
 SET b=2
 SET c=3 KILL a WRITE "in test, at end"
 QUIT

However, when you run test, only variables b and c hold the correct values:

USER>DO ^test
in test, at end
USER>WRITE
b=2
c=3
USER>

The problem in the program is obvious: variable a is KILLed on line 4. However, assume you need to use the debugger to determine this.

You can use the ZBREAK command to set single-stepping through each line of code ("L" action) in the routine test. By a combination of stepping and writing the value of a, you determine that the problem lies in line 4:

USER>NEW 
USER 1S1>ZBREAK 
BREAK
No breakpoints
No watchpoints
USER 1S1>ZBREAK ^test:"L"
USER 1S1>DO ^test 
SET a=1
^
<BREAK>test+1^test
USER 3d3>WRITE a 
<UNDEFINED>^test
USER 3d3>GOTO
SET b=2
^
<BREAK>test+2^test
USER 3d3>WRITE a 
1
USER 3d3>GOTO
SET c=3 KILL a WRITE "in test, at end"
^
<BREAK>test+3^test
USER 3d3>WRITE a
1
USER 3d3>GOTO
in test, at end
QUIT
^
<BREAK>test+4^test
USER 3d3>WRITE a 
WRITE a
^
<UNDEFINED>^test
USER 3d3>GOTO
USER 1S1>

You can now examine that line and notice the KILL a command. In more complex code, you might now want to single-step by command ("S" action) through that line.

If the problem occurred within a DO, FOR, or XECUTE command or a user-defined function, you would use the "L+" or "S+" actions to single-step through lines or commands within the lower level of code.

Understanding ObjectScript Debugger Errors

The ObjectScript Debugger flags an error in a condition or execute argument with an appropriate InterSystems IRIS error message.

If the error is in the execute_code argument, the condition surrounds the execute code when the execute code is displayed before the error message. The condition special variable ($TEST) is always set back to 1 at the end of the execution code so that the rest of the debugger processing code works properly. When control returns to the routine, the value of $TEST within the routine is restored.

Suppose you issue the following ZBREAK command for the example program test:

USER>ZBREAK test+1^test:"B":"a=5":"WRITE b"

In the program test, variable b is not defined at line test+1, so there is an error. The error display appears as follows:

IF a=5 XECUTE "WRITE b" IF 1
^
<UNDEFINED>test+1^test

If you had not defined a condition, then an artificial true condition would be defined before and after the execution code; for example:

USER>IF 1 WRITE b IF 1

Debugging With BREAK

InterSystems IRIS includes three forms of the BREAK command:

  • BREAK without an argument inserted into routine code establishes a breakpoint at that location. When encountered during code execution this breakpoint suspend execution and returns to the Terminal prompt.

  • BREAK with a letter string argument establishes or deletes breakpoints at that enable stepping through code on a line-by-line or command-by-command basis.

  • The BREAK command with an integer argument enables or disables CTRL-C user interrupts. (Refer to the BREAK command for further details.)

Using Argumentless BREAK to Suspend Routine Execution

To suspend a running routine and return the process to the Terminal prompt, enter an argumentless BREAK into your routine at points where you want execution to temporarily stop.

When InterSystems IRIS encounters a BREAK, it takes the following steps:

  1. Suspends the running routine

  2. Returns the process to the Terminal prompt. When debugging an application that uses I/O redirection of the principal device, redirection will be turned off at the debug prompt so output from a debug command will be shown on the Terminal.

    You can now issue ObjectScript commands, modify data, and execute further routines or subroutines, even those with errors or additional BREAKs. If you issue an ObjectScript command from the debug Terminal prompt, this command is immediately executed. It is not inserted into the running routine. This command execution is the same behavior as the ordinary Terminal prompt, with one difference: a command proceeded by a Tab character is executed from the debug Terminal prompt; a command proceeded by a Tab character is not executed from the ordinary Terminal prompt.

To resume execution at the point at which the routine was suspended, issue an argumentless GOTO command.

You may find it useful to specify a postconditional on an argumentless BREAK command so that you can rerun the same code simply by setting the postconditional variable rather than having to change the routine. For example, you may have the following line in a routine:

CHECK BREAK:$DATA(debug)

You can then set the variable debug to suspend the routine and return the job to the Terminal prompt or clear the variable debug to continue running the routine.

For further details, see Command Postconditional Expressions.

Using Argumented BREAK to Suspend Routine Execution

You do not have to place argumentless BREAK commands at every location where you want to suspend your routine. InterSystems IRIS provides several argument options that allow you to step through the execution of the code. You can step through the code by single steps (BREAK “S”) or by command line (BREAK “L”). For a full list of these letter code arguments, see the BREAK command.

One difference between BREAK “S” and BREAK “L” is that many command lines consist of more than one step. This is not always obvious. For example, the following are all one line (and one ObjectScript command), but each is parsed as two steps: SET x=1,y=2, KILL x,y, WRITE “hello”,!, IF x=1,y=2.

Both BREAK “S” and BREAK “L” ignore label lines, comments, and TRY statements (though both break at the closing curly brace of a TRY block). BREAK “S” breaks at a CATCH statement (if the CATCH block is entered); BREAK “L” does not.

When a BREAK returns the process to the Terminal prompt, the break state is not stacked. Thus you can change the break state and the new state remains in effect when you issue an argumentless GOTO to return to the executing routine.

InterSystems IRIS stacks the break state whenever a DO, XECUTE, FOR, or user-defined function is entered. If you choose BREAK "C" to turn off breaking, the system restores the break state at the end of the DO, XECUTE, FOR, or user-defined function. Otherwise, InterSystems IRIS ignores the stacked state.

Thus if you enable breaking at a low subroutine level, breaking continues after the routine returns to a higher subroutine level. In contrast, if you disable breaking at a low subroutine level that was in effect at a higher level, breaking resumes when you return to that higher level. You can use BREAK "C-" to disable breaking at all levels.

You can use BREAK “L+” or BREAK “S+” to enable breaking within a DO, XECUTE, FOR, or a user-defined function.

You can use BREAK “L-” to disable breaking at the current level but enables line breaking at the previous level. You can use BREAK “S-” to disable breaking at the current level but enables single-step breaking at the previous level.

Shutting Off Debugging

To remove all debugging that has been established for a process, use the BREAK "OFF" command. This command removes all breakpoints and watchpoints and turns off stepping at all program stack levels. It also removes the association with the debug and trace devices, but does not close them.

Invoking BREAK "OFF" is equivalent to issuing the following set of commands:

 ZBREAK /CLEAR 
 ZBREAK /TRACE:OFF 
 ZBREAK /DEBUG:"" 
 ZBREAK /ERRORTRAP:ON 
 BREAK "C-"

Terminal Prompt Shows Program Stack Information

When a BREAK command suspends execution of a routine or when an error occurs, the program stack retains some stacked information. When this occurs, a brief summary of this information is displayed as part of the Terminal prompt ( namespace> ). For example, this information might take the form: USER 5d3>, where:

Character Description
5 Indicates there are five stack levels. A stack level can be caused by a DO, FOR, XECUTE, NEW, user-defined function call, error state, or break state.
d Indicates that the last item stacked is a DO.
3 Indicates there are 3 NEW states, parameter passing, or user-defined functions on the stack. This value is a zero if no NEW commands, parameter passing, or user-defined functions are stacked.

Terminal prompt letter codes are listed in the following table.

Stack Error Codes at the Terminal Prompt
Prompt Definition
d DO
e user-defined function
f FOR loop
x XECUTE
B BREAK state
E Error state
N NEW state
S Sign-on state

In the following example, command line statements are shown with their resulting Terminal prompts when adding stack frames:

USER>NEW
USER 1S1>NEW
USER 2N1>XECUTE "NEW  WRITE 123 BREAK"
<BREAK>
USER 4x1>NEW
USER 5B1>BREAK
<BREAK>
USER 6N2>

You can unwind the program stack using QUIT 1. The following is an example of Terminal prompts when unwinding the stack:

USER 6f0>QUIT 1  /* an error occurred in a FOR loop. */
USER 5x0>QUIT 1  /* the FOR loop was in code invoked by XECUTE. */
USER 4f0>QUIT 1  /* the XECUTE was in a FOR loop. */
USER 3f0>QUIT 1  /* that FOR loop was nested inside another FOR loop. */
USER 2d0>QUIT 1  /* the DO command was used to execute the program. */
USER 1S0>QUIT 1  /* sign on state. */
USER>

FOR Loop and WHILE Loop

You can use either a FOR or a WHILE to perform the same operation: loop until an event (usually a counter increment) causes execution to break out of the loop. However, which loop construct you use has consequences for performing single-step (BREAK "S+" or BREAK "L+") debugging on the code module.

A FOR loop pushes a new level onto the stack. A WHILE loop does not change the stack level. When debugging a FOR loop, popping the stack from within the FOR loop (using BREAK "C" GOTO or QUIT 1) allows you to continue single-step debugging with the command immediately following the end of the FOR command construct. When debugging a WHILE loop, issuing a using BREAK "C" GOTO or QUIT 1 does not pop the stack, and therefore single-step debugging does not continue following the end of the WHILE command. The remaining code executes without breaking.

Resuming Execution after a BREAK or an Error

When returned to the Terminal prompt after a BREAK or an error, InterSystems IRIS keeps track of the location of the command that caused the BREAK or error. Later, you can resume execution at the next command simply by entering an argumentless GOTO at the Terminal prompt:

USER 4f0>GOTO

By typing a GOTO with an argument, you can resume execution at the beginning of another line in the same routine with the break or error, as follows:

USER 4f0>GOTO label3

You can also resume execution at the beginning of a line in a different routine:

USER 4f0>GOTO label3^rou

Alternatively, you may clear the program stack with an argumentless QUIT command:

USER 4f0>QUIT
USER>

Sample Dialogs

The following routines are used in the examples below:

MAIN ; 03 Jan 2019 11:40 AM
 SET x=1,y=6,z=8
 DO ^SUB1 WRITE !,"sum=",sum
 QUIT

SUB1 ; 03 Jan 2019 11:42 AM
 SET sum=x+y+z
 QUIT

With BREAK "L", breaking does not occur in the routine SUB1.

USER>BREAK "L"
USER>DO ^MAIN
SET x=1,y=6,z=8
^
<BREAK>MAIN+1^MAIN
USER 2d0>GOTO
DO ^SUB1 WRITE !,"sum=",sum
^
<BREAK>MAIN+2^MAIN
USER 2d0>GOTO
sum=15
QUIT
^
<BREAK>MAIN+3^MAIN
USER 2d0>GOTO
USER>

With BREAK "L+", breaking also occurs in the routine SUB1.

USER>BREAK "L+"
USER>DO ^MAIN
SET x=1,y=6,z=8
^
<BREAK>MAIN+1^MAIN
USER 2d0>GOTO
DO ^SUB1 WRITE !,"sum=",sum
^
<BREAK>MAIN+2^MAIN
USER 2d0>GOTO
SET sum=x+y+z
^
<BREAK>SUB1+1^SUB1
USER 3d0>GOTO
QUIT
^
<BREAK>SUB1+2^SUB1
USER 3d0>GOTO
sum=15
QUIT
^
<BREAK>MAIN+3^MAIN
USER 2d0>GOTO
USER>

The NEW Command at the Terminal Prompt

The argumentless NEW command effectively saves all symbols in the symbol table so you can proceed with an empty symbol table. You may find this command particularly valuable after an error or BREAK.

To run other routines without disturbing the symbol table, issue an argumentless NEW command at the Terminal prompt. The system then:

  • Stacks the current frame on the program stack.

  • Returns the Terminal prompt for a new stack frame.

For example:

USER 4d0>NEW
USER 5B1>DO ^%T
3:49 PM
USER 5B1>QUIT 1
USER 4d0>GOTO

The 5B1> prompt indicates that the system has stacked the current frame entered through a BREAK. The 1 indicates that a NEW command has stacked variable information, which you can remove by issuing a QUIT 1. When you wish to resume execution, issue a QUIT 1 to restore the old symbol table, and a GOTO to resume execution.

Whenever you use a NEW command, parameter passing, or user-defined function, the system places information on the stack indicating that later an explicit or implicit QUIT at the current subroutine or XECUTE level should delete certain variables and restore the value of others.

You may find it useful to know if any NEW commands, parameter passing, or user-defined functions have been executed (thus stacking some variables), and if so, how far back on the stack this information resides.

The QUIT Command at the Terminal Prompt

From the Terminal prompt you can remove all items from the program stack by entering an argumentless QUIT command:

USER 4f0>QUIT
USER>

To remove only a couple of items from the program stack (for example, to leave a currently executing subroutine and return to a previous DO level), use QUIT with an integer argument. QUIT 1 removes the last item on the program stack, QUIT 3 removes the last three items, and so forth, as illustrated below:

9f0>QUIT 3
6d0>

InterSystems IRIS Error Messages

InterSystems IRIS displays error messages within angle brackets, as in <ERROR>, followed by a reference to the line that was executing at the time of the error and by the routine. A caret (^) separates the line reference and routine. Also displayed is the intermediate code line with a caret character under the first character of the command executing when the error occurred. For example:

SET x=y+3 DO ^ABC
^
<UNDEFINED>label+3^rou

This error message indicates an <UNDEFINED> error (that refers to the variable y) in line label+3 of routine rou. At this point, this message is also the value of the special variable $ZERROR.

Using %STACK to Display the Stack

You can use the %STACK utility to:

  • Display the contents of the process execution stack.

  • Display the values of local variables, including values that have been “hidden” with the NEW command or through parameter passing.

  • Display the values of process state variables, such as $IO and $JOB.

Running %STACK

You execute %STACK by entering the following command:

USER>DO ^%STACK

As shown in this example, the %STACK utility displays the current process stack without variables.

Level  Type     Line                   Source 
  1   SIGN ON 
  2   DO                                 ~DO ^StackTest 
  3   NEW ALL/EXCL                       NEW (E) 
  4   DO                 TEST+1^StackTest          SET A=1 ~DO TEST1 QUIT  ;level=2 
  5   NEW                                NEW A 
  6   DO                TEST1+1^StackTest          ~DO TEST2 ;level = 3 
  7   ERROR TRAP                         SET $ZTRAP="TrapLabel^StackTest" 
  8   XECUTE            TEST2+2^StackTest  ~XECUTE "SET A=$$TEST3()" 
  9   $$EXTFUNC                ^StackTest ~SET A=$$TEST3() 
 10   PARAMETER                          AA 
 11   DIRECT BREAK      TEST3+1^StackTest          ~BREAK 
 12   DO                       ^StackTest ~DO ^%STACK 

Under the current execution stack display, %STACK prompts you for a Stack Display Action. You can get help by entering a question mark (?) at this prompt. You can exit %STACK by pressing the Return key at this prompt.

Displaying the Process Execution Stack

Depending on what you enter at the Stack Display Action prompt, you can display the current process execution stack in four forms:

  • Without variables, by entering *F

  • With a specific local variable, by entering *V

  • With all local variables, by entering *P

  • With all local variables, preceded by a list of process state variables, by entering *A

%STACK then displays the Display on Device prompt, enabling you to specify where you want this information to go. Press the Return key to display this information to the current device.

Displaying the Stack without Variables

The process execution stack without variables appears when you first enter the %STACK utility or when you type *F at the Stack Display Action prompt.

Displaying the Stack with a Specific Variable

Enter *V at the Stack Display Action prompt. This will prompt you for the name(s) of the local variable(s) you want to track through the stack. Specify a single variable or a comma-separated list of variables. It returns the names and values of all local variables. In the following example, the variable e is being tracked and the display is sent to the Terminal by pressing Return

Stack Display Action: *V 
Now loading variable information ...   2  done.
Variable(s): e
Display on 
Device: <RETURN>

Displaying the Stack with All Defined Variables

Enter *P at the Stack Display Action prompt to see the process execution stack together with the current values of all defined local variables.

Displaying the Stack with All Variables, including State Variables

Enter *A at the Stack Display Action prompt to display all possible reports. Reports are issued in the following order:

  • Process state intrinsic variables

  • Process execution stack with the names and values of all local variables

Understanding the Stack Display

Each item on the stack is called a frame. The following table describes the information provided for each frame.

%STACK Utility Information
Heading Description
Level Identifies the level within the stack. The oldest item on the stack is number 1. Frames without an associated level number share the level that first appears above them.
Type Identifies the type of frame on the stack, which can be: DIRECT BREAK: A BREAK command was encountered that caused a return to direct mode. DIRECT CALLIN: An InterSystems IRIS process was initiated from an application outside of InterSystems IRIS, using the InterSystems IRIS call-in interface. DIRECT ERROR: An error was encountered that caused a return to direct mode. DO: A DO command was executed. ERROR TRAP: If a routine sets $ZTRAP, this frame identifies the location where an error will cause execution to continue. FOR: A FOR command was executed. NEW: A NEW command was executed. If the NEW command had arguments, they are shown. SIGN ON: Execution of the InterSystems IRIS process was initiated. XECUTE: An XECUTE command was executed. An $XECUTE function was executed. $$EXTFUNC: A user-defined function was executed.
Line Identifies the ObjectScript source line associated with the frame, if available, in the format label+offset^routine.
Source Shows the source code for the line, if it is available. If the source is too long to display in the area provided, horizontal scrolling is available. If the device is line- oriented, the source wraps around and continued lines are preceded with ....

The following table shows whether level, line, and source values are available for each frame type. A "No" under Level indicates that the level number is not incremented and no level number appears in the display.

Frame Types and Values Available
Frame Type Level Line Source
DIRECT BREAK Yes Yes Yes
DIRECT CALL IN Yes No No
DIRECT ERROR Yes Yes Yes
DO Yes Yes* Yes
ERROR TRAP No No No, but the new $ZTRAP value is shown.
FOR No Yes Yes
NEW No No Shows the form of the NEW (inclusive or exclusive) and the variables affected.
PARAMETER No No Shows the formal parameter list. If a parameter is passed by reference, shows what other variables point to the same memory location.
SIGN ON Yes No No
XECUTE Yes Yes* Yes
$$EXTFUNC Yes Yes* Yes
* The LINE value is blank if these are invoked from the Terminal prompt.

Moving through %STACK Display

If a %STACK display fills more than one screen, you see the prompt -- more -- in the bottom left corner of the screen. At the last page, you see the prompt -- fini --. Type ? to see key presses you use to maneuver through the %STACK display.

- - - Filter Help - - -
<space> Display next page. 
<return> Display one more line. 
T Return to the beginning of the output. 
B Back up one page (or many if arg>1). 
R Redraw the current page. 
/text Search for \qtext\q after the current page. 
A View all the remaining text. 
Q Quit. 
? Display this screen 
# specify an argument for B, L, or W actions. 
L set the page length to the current argument. 
W set the page width to the current argument.

You enter any of the commands listed above whenever you see the -- more -- or -- fini -- prompts.

For the B, L and W commands, you enter a numeric argument before the command letter. For instance, enter 2B to move back two pages, or enter 20L to set the page length to 20 lines.

Be sure to set your page length to the number of lines which are actually displayed; otherwise, when you do a page up or down, some lines may not be visible. The default page length is 23.

Displaying Variables at Specific Stack Level

To see the variables that exist at a given stack frame level, enter ?# at the Stack Display Action prompt, where # is the stack frame level. The following example shows the display if you request the variables at level 1.

Stack Display Action: ?1 
The following Variables are defined for Stack Level: 1
E 
Stack Display Action:

You can also display this information using the %SYS.ProcessQueryOpens in a new tab VariableList class query.

Displaying Stack Levels with Variables

You can display the variables defined at all stack levels by entering ?? at the Stack Display Action prompt. The following example shows a sample display if you select this action.

Stack Display Action: ?? 
Now loading variable information ... 19 
Base Stack Level: 5
A
Base Stack Level: 3
A B C D
Base Stack Level: 1
E
Stack Display Action:

Displaying Process State Variables

To display the process state variables, such as $IO, enter *S at the “Stack Display Action” prompt. You will see these defined variables (Process State Intrinsics) as listed in the following table:

Process State Intrinsics Documentation
$D = $DEVICE special variable
$EC = ,M9, $ECODE special variable
$ES = 4 $ESTACK special variable
$ET =  
$H = 64700,50668 $HOROLOG special variable
$I = |TRM|:|5008 $IO special variable
$J = 5008 $JOB special variable
$K = $c(13) $KEY special variable
$P = |TRM|:|5008 $PRINCIPAL special variable
$Roles = %All $ROLES special variable
$S = 268315992 $STORAGE special variable
$T = 0 $TEST special variable
$TL = 0 $TLEVEL special variable
$USERNAME = glenn $USERNAME special variable
$X = 0 $X special variable
$Y = 17 $Y special variable
$ZA = 0 $ZA special variable
$ZB = $c(13) $ZB special variable
$ZC = 0 $ZCHILD special variable
$ZE = <DIVIDE> $ZERROR special variable
$ZJ = 5 $ZJOB special variable
$ZM = RY\Latin1\K\UTF8\ $ZMODE special variable
$ZP = 0 $ZPARENT special variable
$ZR = ^||a $ZREFERENCE special variable
$ZS = 262144 $ZSTORAGE special variable
$ZT = $ZTRAP special variable
$ZTS = 64700,68668.58 $ZTIMESTAMP special variable
$ZU(5) = USER $NAMESPACE
$ZU(12) = c:\intersystems\iris\mgr\ NormalizeDirectory()Opens in a new tab
$ZU(18) = 0 Undefined()Opens in a new tab
$ZU(20) = USER UserRoutinePath()Opens in a new tab
$ZU(23,1) = 5  
$ZU(34) = 0  
$ZU(39) = USER SysRoutinePath()Opens in a new tab
$ZU(55) = 0 LanguageMode()Opens in a new tab
$ZU(56,0) = $Id: //iris/2018.1.1/kernel/common/src/emath.c#1 $ 0  
$ZU(56,1) = 1349  
$ZU(61) = 16  
$ZU(61,30,n) = 262160  
$ZU(67,10,$J) = 1 JobTypeOpens in a new tab
$ZU(67,11,$J) = glenn UserNameOpens in a new tab
$ZU(67,12,$J) = TRM: ClientNodeNameOpens in a new tab
$ZU(67,13,$J) = ClientExecutableNameOpens in a new tab
$ZU(67,14,$J) = CSPSessionIDOpens in a new tab
$ZU(67,15,$J) = 127.0.0.1 ClientIPAddressOpens in a new tab
$ZU(67,4,$J) = 0^0^0 StateOpens in a new tab
$ZU(67,5,$J) = %STACK RoutineOpens in a new tab
$ZU(67,6,$J) = USER NameSpaceOpens in a new tab
$ZU(67,7,$J) = |TRM|:|5008 CurrentDeviceOpens in a new tab
$ZU(67,8,$J) = 923 LinesExecutedOpens in a new tab
$ZU(67,9,$J) = 46 GlobalReferencesOpens in a new tab
$ZU(68,1) = 0 NullSubscripts()Opens in a new tab
$ZU(68,21) = 0 SynchCommit()Opens in a new tab
$ZU(68,25) = 0  
$ZU(68,27) = 1  
$ZU(68,32) = 0 ZDateNull()Opens in a new tab
$ZU(68,34) = 1 AsynchError()Opens in a new tab
$ZU(68,36) = 0  
$ZU(68,40) = 0 SetZEOF()Opens in a new tab
$ZU(68,41) = 1  
$ZU(68,43) = 0 OldZU5()Opens in a new tab
$ZU(68,5) = 1 BreakMode()Opens in a new tab
$ZU(68,6) = 0  
$ZU(68,7) = 0 RefInKind()Opens in a new tab
$ZU(131,0) = MYCOMPUTER  
$ZU(131,1) = MYCOMPUTER:IRIS  
$ZV = IRIS for Windows (x86-64) 2018.1.0 (Build 527U) Tue Feb 20 2018 22:47:10 EST $ZVERSION special variable

Printing the Stack and/or Variables

When you select the following actions, you can choose the output device:

  • *P

  • *A

  • *V after selecting the variables you want to display.

Other Debugging Tools

There are also other tools available to aid in the debugging process. These include:

Displaying References to an Object with $SYSTEM.OBJ.ShowReferences

To display all variables in the process symbol table that contain a reference to a given object, use the ShowReferences(oref)Opens in a new tab method of the %SYSTEM.OBJOpens in a new tab class. The oref is the OREF (object reference) for the given object. For details on OREFs, see OREF Basics.

Error Trap Utilities

The error trap utilities, %ETN and %ERN, help in error analysis by storing variables and recording other pertinent information about an error.

%ETN Application Error Trap

You may find it convenient to set the error trap to execute the utility %ETN on an application error. This utility saves valuable information about the job at the time of the error, such as the execution stack and the value of variables. This information is saved in the application error log, which you can display with the %ERN utility or view in the Management Portal on the View Application Error Log page (System Operation, System Logs, Application Error Log).

Use the following code to set the error trap to this utility:

SET $ZTRAP="^%ETN"
Note:

In a procedure, you cannot set $ZTRAP to an external routine. Because of this restriction, you cannot use ^%ETN in procedures (including class methods that are procedures). However, you can set $ZTRAP to a local label that calls %ETN.

When an error occurs and you call the %ETN utility, you see a message similar to the following message:

Error has occurred: <SYNTAX> at 10:30 AM

Because %ETN ends with a HALT command (terminates the process) you may want to set the %ETN error trap only if the routine is used in Application Mode. When an error occurs at the Terminal prompt, it may be useful for the error to be displayed on the terminal and go into the debugger prompt to allow for immediate analysis of the error. The following code sets an error trap only if InterSystems IRIS is in Application Mode:

SET $ZTRAP=$SELECT($ZJ#2:"",1:"^%ETN")

%ERN Application Error Report

The %ERN utility examines application errors recorded by the %ETN error trap utility. See Using %ERN to View Application Error Logs.

In the following code, a ZLOAD of the routine REPORT is issued to illustrate that by loading all of the variables with *LOAD and then loading the routine, you can recreate the state of the job when the error occurred except that the program stack, which records information about DOs, etc., is empty.

USER>DO ^%ERN

For Date: 4/30/2018   3 Errors

Error: ?L

1) "<DIVIDE>zMyTest+2^Sample.MyStuff.1"  at 10:27 am.   $I=|TRM|:|10044   ($X=0  $Y=17)
     $J=10044  $ZA=0   $ZB=$c(13)   $ZS=262144 ($S=268242904)
               WRITE 5/0

2) <SUBSCRIPT>REPORT+4^REPORT at 03:16 pm. $I=|TRM|:|10044   ($X=0  $Y=57)
     $J=10044  $ZA=0   $ZB=$c(13)   $ZS=2147483647 ($S=2199023047592)
 SET ^REPORT(%DAT,TYPE)=I

3) <UNDEFINED>zMyTest+2^Sample.MyStuff.1 *undef"  at 10:13 pm.   $I=|TRM|:|12416   ($X=0  $Y=7)
     $J=12416  $ZA=0   $ZB=$c(13)   $ZS=262144 ($S=268279776)
               WRITE undef

Error: 2

2) <SUBSCRIPT>REPORT+4^REPORT at 03:16 pm. $I=|TRM|:|10044   ($X=0  $Y=57)
     $J=10044  $ZA=0   $ZB=$c(13)   $ZS=2147483647 ($S=2199023047592)
 SET ^REPORT(%DAT,TYPE)=I

Variable: %DAT
  %DAT="Apr 30 2018" 

Variable: TYPE
  TYPE="" 

Variable: *LOAD
USER>ZLOAD REPORT

USER>WRITE

%DAT="Apr 30 2018"
%DS=""
%TG="REPORT+1"
I=88
TYPE=""
XY="SET $X=250 WRITE *27,*91,DY+1,*59,DX+1,*72 SET $X=DX,$Y=DY"
USER>

FeedbackOpens in a new tab