This is another one where I’m not sure how many people need it, but here it is anyway: debugToolKit github repo.
This is for implementing simple, special-purpose interactive debuggers in-game.
In this context “debugger” means a lightweight, modal interface that’s only available in during development and is disabled for release.
A ways back I wrote a general interactive debugger that’s mostly designed to view the stack in a running TADS3 game, called either from the command line, programmatically, or via exception handler.
And then a little while ago when writing a module for transcript/report re-writing I found myself wanting transcript-specific debugging commands. I initially tried just integrating new commands into the old debugTool
module, but the nature of how it works is a little ideosyncratic—it’s twiddling the stack but it’s also on the stack, so command execution is a little fiddly because you have to worry about whether something is being run “directly” or after a context switch.
Anyway, because of those difficulties I ended up writing a simple standalone transcript debugger as more or less a one-off, but patterned after the work I’d done on debugTool
.
That got me thinking about all the other debugging stuff I’ve implemented. Usually my approach had been to implement debugging commands as bespoke T3 actions that are executed by the normal TADS command execution process.
The present module is intended to make it easier to group related debugging commands/activities into a single modal interface.
USAGE
I’ll just dive in with an example. In this case just using the default debugger, which doesn’t do much:
// Declare a debugger.
demoDebugger: DtkDebugger;
Now code can drop execution into the debugger (such as it is) by calling demoDebugger.debugger()
. Usage for the debugger()
method is:
debugger(data, t?, lbl?)
data
: An arbitrary data object. Presumably whatever you want to debug, but it’s left to the instance to decide what, if anything, to do with it.
No default.t
: Optional arg giving the transcript. This transcript will be disabled while the debugger is running, and then restored when the debugger exits.
Defaults togTranscript
.lbl
Optional label for the debugger startup banner. This is intended to clarify what invoked the debugger.
With no label specified the debugger will start with===breakpoint in unknown===
, with the label replacing “unknown” if specified.
By default the debugger includes the following commands:
exit
Exits the debugger, returning to the game.help
Displays help for the debugger. The help command automagically enumerates the available debugger commands along with a short description of each.
You can also use “help [command]” to get an extended help message for the given debugger command.
If we define a >FOOZLE
command to drop into the debugger, then the stock debugger experience looks something like:
Void
This is a featureless void.
You see a pebble here.
>foozle
===breakpoint in command line===
===type HELP or ? for information on the interactive debugger===
>>> ?
exit exit the debugger
help display this message. use "help [command]" for more information
>>> help exit
Use "exit" to exit the debugger and returh to the game.
>>> exit
Exiting debugger.
>
Not particularly useful by itself.
DEFINING NEW COMMANDS
In order to do useful debugging, we’ll have to declare our own debugging commands.
The module provides a DtkCommand
class to do that. The interesting properties are:
id
A single-quoted string to use as the command keyword.
Example:'exit'
help
A single-quoted string to use as the short help message. That’s what’s displayed alonside the command keyword in the “help” listing.
Example:'exit the debugger'
longHelp
A double-quoted string used as the long help message. This is displayed for “help [command]” for the command.
Example:"The <q>exit</q> command exits the debugger."
argCount
The number of arguments this command accepts
Default is 0.hidden
A boolean flag. Iftrue
, the “help” command won’t list this command.
Default isnil
.cmd(args...)
The method called to execute the command. It will be called with whatever args (if any) are given to the debugger input.
IMPORTANT: This method needs to returnnil
(or not return an explicit return value) if the debugger should continue operation. Returningtrue
will cause the debugger to exit after command execution.output(txt, n?)
Convenience method to output messages via the debugger.
The first argument is the text to output.
The optional second arg is the indentation level to use. Default is zero, or no indentation.
There’s also a template to make it easier to declare commands:
DtkCommand 'id' +argCount? 'help'? "longHelp"?;
Commands are added to debuggers via standard TADS3 lexical ownership syntax:
// Declare a debugger.
demoDebugger: DtkDebugger;
// Add a simple command.
+DtkCommand 'foo' 'print the word <q>foo</q>'
"This command prints the word <q>foo</q>. "
cmd() {
output('<q>Foo</q>.');
}
;
Having defined the above, now our debugger looks like:
>foozle
===breakpoint in command line===
===type HELP or ? for information on the interactive debugger===
>>> ?
foo print the word "foo"
exit exit the debugger
help display this message. use "help [command]" for more information
>>> help foo
This command prints the word "foo".
>>> foo
"Foo".
>>>
By default command arguments are treated as string literals. Preface an argument with @
to refer to the given object. Example:
+DtkCommand 'look' +1 'displays an Thing\'s desc'
"Use <b>look @[object name]</b> to display a Thing's desciption. "
cmd(obj) {
if(!obj.ofKind(Thing)) {
output('Argument is not a Thing.');
return;
}
obj.desc();
}
;
This defines a “look @[object]” debugger command that requires a single argument (note the +1
in the first line of the declaration). In action:
>foozle
===breakpoint in command line===
===type HELP or ? for information on the interactive debugger===
>>> ?
foo print the word "foo"
look displays an Thing's desc
exit exit the debugger
help display this message. use "help [command]" for more information
>>> look @pebble
A small, round pebble.
>>> look @foo
Argument is not a Thing.
>>> look adasdasd
Argument is not a Thing.
>>>
EXPRESSION EVALUATOR
In addition to “normal” commands the module supplies a simple TADS3 expression evaluator that can optionally be added to debuggers.
The class is DtkEval
, and it can be added like other commands:
// Declare a debugger.
demoDebugger: DtkDebugger;
// Add the expression evaluator.
+DtkEval;
Now in the debugger the “eval” command will put the debugger in expression evaluator mode. In this mode the prompt will change to eval>>>
and input will be treated as TADS3 source code—compiled (if possible) and then executed.
Back to our example, extended again. If we declare an object foo
with a property bar
whose value is initially nil
:
>foozle
===breakpoint in command line===
===type HELP or ? for information on the interactive debugger===
>>> ?
foo print the word "foo"
look displays an Thing's desc
eval switch to expression evaluator mode
exit exit the debugger
help display this message. use "help [command]" for more information
>>> eval
eval>>> foo.bar
nil
eval>>> foo.bar = 123
123
eval>>> foo.bar
123
eval>>> exit
>>>
Looking at each eval-mode input, that’s:
eval>>> foo.bar
nil
We’re evaluating the expression foo.bar
, and the debugger is displaying the return value of the expression. That’s the value of foo.bar
, which is currently nil
. Next:
eval>>> foo.bar = 123
123
We’re evaluating the expression foo.bar = 123
. The return value of this is the return value of the assignment, which in TADS3 is the value assigned. In this case 123
. Finally:
eval>>> foo.bar
123
We evaluate foo.bar
again, which again will just return the value of foo.bar
. This time it’s 123
, which is what is displayed.
DECLARING DEBUGGER ACTIONS
Finally, the module provides a macro for declaring actions to drop into debuggers. Syntax is simple:
DefineDtkAction(Foozle, 'foozle', demoDebugger);
This will create an action FoozleAction
, which is invoked (in the TADS3 parser) via “foozle”. >FOOZLE
will call demoDebugger.debugger(nil, nil, 'command line')
. This can be changed by overwriting startDebugger()
:
DefineDtkAction(Foozle, 'foozle', demoDebugger)
startDebugger(obj) {
obj.debugger('wibble-wobble', nil, 'the foozle command');
}
;
This will change the banner announcement label to be “the foozle command”:
>foozle
===breakpoint in the foozle command===
===type HELP or ? for information on the interactive debugger===
>>>
…and the data passed to the debugger instance will, for some reason, be the string 'wibble-wobble'
.
TOGGLING THE DEBUGGER
Everything in the module is in big preprocessor conditionals, so you can toggle everything on by compiling with -D DTK
and turn everything off by compiling without it.
Everything should degrade gracefully when compiled with the debugger stuff off—there are stub classes so debugger declarations and debugger action declarations and so on can be left in your code, they’ll just end up being empty classes/NOPs.
CONCLUSION
Again, not sure how helpful this will be to anyone else. I find myself regularly wrestling with the lack of a proper external debugger for TADS3 under linux, so I end up writing a lot of tools to make debugging easier.