Debugging q code¶
When a q function fails, the error already tells you which function failed and
which primitive inside it. The q)) debugger then lets you look inside the
suspended function, walk up to its callers, and either resume or abandon the
call. This guide chases one bad order through a small batch calculation, then
interrupts slow and runaway loops, adds a temporary breakpoint and logs a
backtrace without suspending.
PeachQ 0.91 or later
Earlier releases suspend at q)) and have .Q.bt and .Q.trp. The caret
under the failing primitive, suspending and continuing after Ctrl-C, traps
that catch Ctrl-C, and some stack and backtrace details shown here arrived
in 0.91.
Start PeachQ with ./q and enter the examples at its console. q) and q))
mark what you enter;
the following lines show the result. For the full list of debugger commands, see
the debugging reference.
A batch with one bad order¶
Each order has a quantity and a price. orderValue multiplies them for one
order, and batchTotal adds the values for a whole batch. The third order
arrived with the symbol `tbd in its price field:
q)batch:([] id:1 2 3; qty:100 50 200; price:(10.5;20.25;`tbd))
q)orderValue:{[o] o[`qty]*o`price}
q)batchTotal:{[b] sum orderValue each b}
q)batch
| id | qty | price |
| long | long | |
|------|------|-------|
| 1 | 100 | 10.5 |
| 2 | 50 | 20.25 |
| 3 | 200 | `tbd |
q)batchTotal 2#batch
2062.5
The first two orders total 2062.5. The empty type under price is a hint:
the column is a mixed list, not floats.
Read the error¶
An error in an expression typed at the q) prompt prints and returns to q):
The first line is the error name. The next line is a stack frame: its number in brackets, then the source. The caret sits under the primitive that failed, here the multiplication.
An error inside a function prints the same way, then suspends execution:
Frame [2] is orderValue, called from batchTotal at frame [1], called
from your input at frame [0]. The caret points at the *. The extra ) in
the prompt means the call is suspended: orderValue is still running, and you
are inside it.
Look inside the suspended function¶
At q)), expressions run in the scope of the suspended function, so its
arguments and locals are available by name:
.z.ex holds the primitive that failed and .z.ey its arguments:
The failure is 200*`tbd: order 3 has a symbol where a price belongs.
Walk the stack¶
Enter ` to move up one frame to the caller and . to move back down.
Each move prints the frame you arrive at:
Now names resolve in batchTotal, so b is the whole batch. Find every order
whose price is not a float atom (type -9h):
Only row 2, the order with id 3, is bad. Moving further up reaches your input,
and . comes back down:
& repeats the error and the current frame. .Q.bt[] prints the whole stack,
with >> marking where you are:
q))&
'type
[1] batchTotal:{[b] sum orderValue each b}
^
q)).Q.bt[]
[2] orderValue:{[o] o[`qty]*o`price}
^
>>[1] batchTotal:{[b] sum orderValue each b}
^
[0] batchTotal batch
^
q)).
[2] orderValue:{[o] o[`qty]*o`price}
^
Resume, abort or signal¶
:value resumes the suspended call, using value as the result of the
operation that failed. Here 0f stands in for 200*`tbd, so orderValue
returns 0f for order 3 and the batch finishes:
That total quietly leaves out order 3, so treat resuming as a way to see what happens next rather than a fix.
To abandon the call instead, enter \. Debug sessions can nest: an error in a
function you call at q)) suspends again, one level deeper, and each \ leaves
one level. The deeper frame numbers count the frames of the outer suspension
and of the debugger itself:
q)batchTotal batch
'type
[2] orderValue:{[o] o[`qty]*o`price}
^
q))orderValue o
'type
[5] orderValue:{[o] o[`qty]*o`price}
^
q)))o
id | 3
qty | 200
price| `tbd
q)))\
q))
Entering 'name at q)) signals that error from the deepest frame and removes
the frame, leaving you suspended in its caller:
With the debugger closed, fix the data and run the batch again:
q)fixed:update price:20.0 from batch where id=3
q)fixed
| id | qty | price |
| long | long | float |
|------|------|-------|
| 1 | 100 | 10.5 |
| 2 | 50 | 20.25 |
| 3 | 200 | 20 |
q)batchTotal fixed
6062.5
The price column is now float, and all three orders count.
Interrupt a long-running loop¶
Ctrl-C does not have to end a long calculation. countTo is a slow but correct
loop:
Press Ctrl-C while it runs. If it finishes first, add a zero to n. q signals 'stop and suspends inside the loop, so
you can check its progress. The caret and the value of i depend on when you
press the keys:
A bare : continues the loop from where it stopped, and the call returns its
result as normal:
Inside Trap or .Q.trp, Ctrl-C does not suspend. The handler receives stop
and q stays at q):
Stop a runaway loop¶
firstGap should return the first id missing from a list, but it never
returns:
Press Ctrl-C and look at the loop counter. The caret depends on when you press the keys:
i is still 1 because i+1 computes a value and throws it away. The update
needs i+:1. Abort, fix and run it again:
Add a temporary breakpoint¶
To inspect a function that does not fail, insert a signal where you want it to
stop. Here 'bp stops batchTotal after it computes the order values and
before it adds them:
q)batchTotal:{[b] v:orderValue each b;'bp;sum v}
q)batchTotal fixed
'bp
[1] batchTotal:{[b] v:orderValue each b;'bp;sum v}
^
q))v
1050 1012.5 4000
q)):
6062.5
A bare : resumes, and the function carries on to return sum v. Restore the
original definition when you are done, so the breakpoint does not stop the next
caller:
Log a backtrace instead of suspending¶
A server or scheduled job cannot wait at q)). Trap
catches the error and keeps going, but its handler receives only the error name:
That does not say which order or which function failed. .Q.trp works like
trap, and also passes the backtrace to the handler as a second argument.
.Q.sbt formats it as text and 2@ writes it to stderr. The handler returns
0n as the result:
q).Q.trp[batchTotal;batch;{2@"batchTotal failed: ",x,"\n",.Q.sbt y;0n}]
batchTotal failed: type
[3] orderValue:{[o] o[`qty]*o`price}
^
[2] batchTotal:{[b] sum orderValue each b}
^
[1] (.Q.trp)
[0] .Q.trp[batchTotal;batch;{2@"batchTotal failed: ",x,"\n",.Q.sbt y;0n}]
^
0n
q)
The log names the function and the failing primitive, and q stays at q).
When the batch is good, the handler is not called:
Differences from kdb+¶
These PeachQ behaviours affect the techniques above:
-
Some keywords do not suspend inside themselves. The debugging reference notes that many kdb+ keywords are defined as lambdas and can suspend inside their own code. PeachQ implements many keywords, such as
ssr, natively, so the error suspends in your function, with the caret under the keyword. Keywords still defined in q, such asxasc, can suspend inside their own code as in kdb+: -
Deep recursion signals
'limit. The kdb+ error list documents'stackfor running out of stack space. PeachQ signals'limitafter about 2,000 nested calls and suspends in the deepest one. This is not the kdb+'limiterror, which reports a list too long to create: