Debugging

Contents

Overview

XMF provides a range of debugging functionality. There is essentially two modes of debugging: command line debugging and Eclipse IDE-debugging. The IDe debugging integrates XMF with the Eclipse debugging framework and allows you to set breakpoints in XOCL code and to step through an application (including the XMF system itself). The IDE-debugging environment for XMF on Eclipse is describes elsewhere. This document describes command line debugging facilities in XMF.

To top.

Tracing Operation Calls

Operation calls (including message passing) can be traced using the following operations:
// Print information about an operation each time it
// is called. If more than one operation is traced
// then indentation is used to show the nesting of
// the calls...
Operation::trace()
// Suppress tracing output for an operation...
Operation::untrace()
// Trace all the operations defined in the name-space...
NameSpace::traceAll()
// Untrace all the operations defined in the name-space...
NameSpace::untraceAll()
For example, suppose we have a package of operations:
context Root
@Package MyOps
@Operation test1(n:Integer)
if n > 0
then test2(n-1)
else n
end
end
@Operation test2(n:Integer)
if n > 0
then test1(n-1)
else n
end
end
end
One of the operations can be traced by:
MyOps::test1.trace()
the call:
MyOps::test1(10);
produces the following output:
Enter null.test1(10)
Enter null.test1(8)
Enter null.test1(6)
Enter null.test1(4)
Enter null.test1(2)
Enter null.test1(0)
Exit null.test1 = 0
Exit null.test1 = 0
Exit null.test1 = 0
Exit null.test1 = 0
Exit null.test1 = 0
Exit null.test1 = 0
The 'null' is showing the value of the target of the message (in this case no message is sent). Adding the other operation:
MyOps::test2.trace()
produces:
Enter null.test1(10)
Enter null.test2(9)
Enter null.test1(8)
Enter null.test2(7)
Enter null.test1(6)
Enter null.test2(5)
Enter null.test1(4)
Enter null.test2(3)
Enter null.test1(2)
Enter null.test2(1)
Enter null.test1(0)
Exit null.test1 = 0
Exit null.test2 = 0
Exit null.test1 = 0
Exit null.test2 = 0
Exit null.test1 = 0
Exit null.test2 = 0
Exit null.test1 = 0
Exit null.test2 = 0
Exit null.test1 = 0
 Exit null.test2 = 0
Exit null.test1 = 0
The tracing could have been achieved by:
MyOps.traceAll()
and switched off by:
MyOps.untraceAll()
Normally, you would trace operations in your application. Since much of XMF is written in XOCL you can also trace parts of the system. For example:
OCL.traceAll()
then:
[1] XMF> if 10 > 20 then Class.toString() else Object.toString() end;
Enter If(BinExp(IntExp(10),>,IntExp(20)),Send(Var(Class,2),toString,Seq{}),Send(Var(Object,2),toString,Seq{})).eval(Loop(),[],Seq{<Package XCore>,<NameSpace Root>})
Enter BinExp(IntExp(10),>,IntExp(20)).eval(Loop(),[],Seq{<Package XCore>,<NameSpace Root>})
Enter IntExp(20).eval(Loop(),[],Seq{<Package XCore>,<NameSpace Root>})
Exit IntExp(20).eval = 20
Enter IntExp(10).eval(Loop(),[],Seq{<Package XCore>,<NameSpace Root>})
Exit IntExp(10).eval = 10
Exit BinExp(IntExp(10),>,IntExp(20)).eval = false
Enter Send(Var(Object,2),toString,Seq{}).eval(Loop(),[],Seq{<Package XCore>,<NameSpace Root>})
Enter Var(Object,2).eval(Loop(),[],Seq{<Package XCore>,<NameSpace Root>})
Exit Var(Object,2).eval = <Class Object>
Exit Send(Var(Object,2),toString,Seq{}).eval = <Class Object>
Exit If(BinExp(IntExp(10),>,IntExp(20)),Send(Var(Class,2),toString,Seq{}),Send(Var(Object,2),toString,Seq{})).eval = <Class Object>
<Class Object>
[1] XMF>
and:
[1] XMF> let x = 10 in x + 1 end;
Enter Let(Seq{ValueBinding(x,NamedType(Seq{XCore,Element}),IntExp(10))},BinExp(Var(x,2),+,IntExp(1))).eval(Loop(),[],Seq{<Package XCore>,<NameSpace Root>})
Enter ValueBinding(x,NamedType(Seq{XCore,Element}),IntExp(10)).desugar()
Exit ValueBinding(x,NamedType(Seq{XCore,Element}),IntExp(10)).desugar = ValueBinding(x,NamedType(Seq{XCore,Element}),IntExp(10))
Enter IntExp(10).eval(Loop(),[],Seq{<Package XCore>,<NameSpace Root>})
Exit IntExp(10).eval = 10
Enter Let(Seq{ValueBinding(x,NamedType(Seq{XCore,Element}),IntExp(10))},BinExp(Var(x,2),+,IntExp(1))).dropDeclarations(BinExp(Var(x,2),+,IntExp(1)))
Enter Let(Seq{ValueBinding(x,NamedType(Seq{XCore,Element}),IntExp(10))},BinExp(Var(x,2),+,IntExp(1))).isDeclaration(BinExp(Var(x,2),+,IntExp(1)))
Exit Let(Seq{ValueBinding(x,NamedType(Seq{XCore,Element}),IntExp(10))},BinExp(Var(x,2),+,IntExp(1))).isDeclaration = false
Exit Let(Seq{ValueBinding(x,NamedType(Seq{XCore,Element}),IntExp(10))},BinExp(Var(x,2),+,IntExp(1))).dropDeclarations = BinExp(Var(x,2),+,IntExp(1))
Enter BinExp(Var(x,2),+,IntExp(1)).eval(Loop(),[x=10],Seq{<Package XCore>,<NameSpace Root>})
Enter IntExp(1).eval(Loop(),[x=10],Seq{<Package XCore>,<NameSpace Root>})
Exit IntExp(1).eval = 1
Enter Var(x,2).eval(Loop(),[x=10],Seq{<Package XCore>,<NameSpace Root>})
Exit Var(x,2).eval = 10
Exit BinExp(Var(x,2),+,IntExp(1)).eval = 11
Exit Let(Seq{ValueBinding(x,NamedType(Seq{XCore,Element}),IntExp(10))},BinExp(Var(x,2),+,IntExp(1))).eval = 11
11
[1] XMF>
To top.

Exception Backtraces

When exceptions are created (usually at the same point at which they are thrown) the exception captures the current execution state which can be subsequently displayed in order to provide information that is useful for debugging. Here is a simple example of how this works. Given the following definition:
context Root
@Operation throwError(n:Integer)
if n = 0
then throw Error("Reached 0")
else somethingElse(throwError(n-1))
end
end
then the following top-level command produces an exception that is caught and displayed by the top-level command loop:
[1] XMF> throwError(10);

Exception: Reached 0
Source file position: line = 0
Error(Reached 0)


The following backtrace shows recent message calls:

Root::throwError(0) aborted at line = 116
Root::throwError(1) aborted at line = 115
Root::throwError(2) aborted at line = 115
Root::throwError(3) aborted at line = 115
Root::throwError(4) aborted at line = 115
Root::throwError(5) aborted at line = 115
Root::throwError(6) aborted at line = 115
Root::throwError(7) aborted at line = 115
Root::throwError(8) aborted at line = 115
Root::throwError(9) aborted at line = 115
Root::throwError(10) aborted at line = 115
Loop::readEvalPrint(Engine(),[1] XMF> ,Pair[
left = Binding[name = return,value = <Op return>],
right = Pair[
left = Binding[
name = <a String>,
value = <a CompiledOperation>],
right = NullEnv[]]]) aborted at line = 182
Loop::anonymous() aborted at line = 121
Loop::loop() aborted at line = 72
?::anonymous(<Op anonymous>) aborted at line = 72
?::anonymous(Seq{Seq{...}})

[1] XMF>
The exception message (Reached 0) is displayed and then a backtrace is displayed that shows the calls that have built up during the execution of the top-level command. You can display the backtrace yourself using Exception::printBacktrace which takes a single boolean argument. If the argument is true then the exception is displayed in brief (as shown above). If the argument is false then the backtrace is shown in full, including argument and local variables:
Exception: Reached 0
Source file position: line = 0
Error(Reached 0)


The following backtrace shows recent message calls:

Root::throwError(n) aborted at line = 116

Argument values:
n = 0
Target of message:
self = null

----------------------------------

Root::throwError(n) aborted at line = 115

Argument values:
n = 1
Target of message:
self = null

----------------------------------

Root::throwError(n) aborted at line = 115

Argument values:
n = 2
Target of message:
self = null

----------------------------------

Root::throwError(n) aborted at line = 115

Argument values:
n = 3
Target of message:
self = null

----------------------------------

Root::throwError(n) aborted at line = 115

Argument values:
n = 4
Target of message:
self = null

----------------------------------

Root::throwError(n) aborted at line = 115

Argument values:
n = 5
Target of message:
self = null

----------------------------------

Root::throwError(n) aborted at line = 115

Argument values:
n = 6
Target of message:
self = null

----------------------------------

Root::throwError(n) aborted at line = 115

Argument values:
n = 7
Target of message:
self = null

----------------------------------

Root::throwError(n) aborted at line = 115

Argument values:
n = 8
Target of message:
self = null

----------------------------------

Root::throwError(n) aborted at line = 115

Argument values:
n = 9
Target of message:
self = null

----------------------------------


...more frames (increase Exception::backtraceLimit).
true
[1] XMF>
The most recently thrown exception that is handled by the top-level command loop is bound to the global variable xx. The top-level command ?pp x displays in long form the value of xx.

If you create your own exception types, then you should be careful to initialize the exception correctly via the constructor. The operation setBacktrace should be called in order to capture the current call-stack state. Here is an example of a simple exception definition:
context Exceptions

@Class FileNotFound extends Exception

@Attribute path : String end

@Constructor(path) !
self.setBacktrace();
self.message := "The file " + path + " cannot be found."
end

end
To top.

Type Checking

XMF is dynamically typed which means that it checks types just before it attempts to perform a VM operation. By default, type declarations for arguments, local variables and return values have no effect (although they can make the code more readable). The compiler has a switch:
Compiler::checkTypes
that is used to control whether type checking code is generated from type declarations. An easy way to modify this switch is using the top-level command:
?o checkTypes true
Consider the following simple operation:
context Root
@Operation typeCheck(b:Boolean,s:Seq(Integer)):Boolean
let n:Integer = s->iterate(n i = 0 | i + n)
in b or n > 10
end
end
If the operation is compiled and loaded with the checkTypes compiler switch set to true then the types of the arguments, local variables and return value will be checked at run-time. Suppose that we call the operation with elements of the wrong types:
[1] XMF> typeCheck(10,Seq{1,2,3});

Exception: The arg b of operation typeCheck1 expects a value of type Root::XCore::Boolean got 10 of type Root::XCore::I
Source file position: line = 0
ArgTypeError(typeCheck1,b,10,<DataType Boolean>)


The following backtrace shows recent message calls:
...
and:
[1] XMF> typeCheck1(true,Seq{"a","b","c"});

Exception: The arg s of operation typeCheck1 expects a value of type Seq(Integer) got Seq{a,b,c} of type Root::XCore::S
Source file position: line = 0
ArgTypeError(typeCheck1,s,Seq{a,b,c},<Seq Seq(Integer)>)


The following backtrace shows recent message calls:
...
Now, suppose we made a mistake in the type of the local variable:
context Root
@Operation typeCheck(b:Boolean,s:Seq(Integer)):Boolean
let n:Integer = s->iterate(n i = 0 | i + n) > 10
in b or n > 10
end
end
Calling the operation with the correct argument values, causes the following exception:
[1] XMF> typeCheck2(true,Seq{1,2,3});

Exception: The local n expects a value of type Root::XCore::Integer got false of type Root::XCore::Boolean
Source file position: line = 0
LocalTypeError(n,false,<DataType Integer>)


The following backtrace shows recent message calls:
...
Finally, if the return value disagrees with the declared type for the operation:
context Root
@Operation typeCheck3(b:Boolean,s:Seq(Integer)):Integer
let n:Integer = s->iterate(n i = 0 | i + n)
in b or n > 10
end
end
then the following exception is generated:
[1] XMF> typeCheck3(true,Seq{1,2,3});

Exception: Operation typeCheck3 expects to return a value of type Root::XCore::Integer but returned true of type Root::
Source file position: line = 0
ResultTypeError(typeCheck3,true,<DataType Integer>)


The following backtrace shows recent message calls:
...
To top.