TADS 3 Debugging Tutorial By Brett Witty

People in the TADS 3 study group have been looking at the workbench debugger and wondering how to use it, so I've decided to step up and impart what I know of debuggers. This tutorial will run through debugging a small game to get an idea of how to use the debugger and give a few little tips and tricks I've picked up. It's more a read-think-understand-try sort of tutorial than a plain list of instructions.

What you will need

You'll need a bunch of things to make sure you're set up correctly and can follow this tutorial without hitches.

If you have any questions or feedback, send me an email.

So what is debugging?

The whole idea of programming is to write a bunch of instructions that you want a computer to perform for you. Humans and computers definitely don't speak the same language, so you need a few intermediaries to translate your thoughts into things the computer will do. The first thing you need to do is write instructions down in a programming language. This isn't like English, but a restricted language that allows you to capture the ideas you want to work with. TADS 3 is a programming language focussed solely on capturing descriptions of Interactive Fiction. Inform 7 has similar goals but with a different way of speaking. Other more technical languages like C/C++ or Java have a much broader scope, so they have a lot more technicalities. TADS 3 has a smaller scope, so it's simpler.

Now the thing is that computers don't even speak TADS 3. We need something to translate this into computer language, and this is what the compiler does. It takes things like:

startRoom = apartment
into a bunch of binary that makes sense to the computer. You don't need to care how this is done or what it turns into to write IF games, but you have to be aware that this stuff exists. Compiling is what takes all that time before you hit "Compile and Run".

This all works fine until the computer takes your lovely code and does something brain-dead instead. You have to figure out why there's a difference between what you thought you wrote and what the program did. Sometimes it's because you thought you wrote something, but upon reflection, you actually told the computer to do something else. Sometimes you've told it the right thing but due to the way the whole game is working, some assumption is wrong (like an object was in the room when it isn't). This causes things to go wrong. Very rarely, you've done everything right and there is a bug in the TADS 3 program. Figuring out what is going on and why your game isn't working how you think it should is called debugging.

The whole practice of debugging is to watch the computer in motion and check it with your mental model and seeing where things go wrong. We can't watch it in real-time because computers work really, really fast. Moreover, computers are really, really dumb so you have to spell things out. For example, "Put all the objects on the floor into the bag" ends up being translated into "Put the red block in the bag, and put the blue block in the bag, and..." where even the "put x in the y" expands out to a bunch of commands like "Remember x. Change its location data to y. Now tell the old place and the new place that you've moved it so all the bookkeeping works out." This expansion of a simple idea into a large number of dumb commands is how programming works. Computers work because they can do these dumb things crazy fast.

So we can't watch it in real-time, what can we do? Have you ever had someone explain something to you too fast and you had to stop them and go over it again? For example, your friend might give you directions to a new pizza place: "So from your place go left on main, then second right go along two blocks then left past the coffee shop until you get to the lights then turn right."
You stop them and go "Whoa, whoa, whoa... Back up. So from my place, we go left? You mean left at the end of the street?"
Your friend says, "Yep."
"Okay, then wait until the second right..."
"Yep."
"Wait, the coffee shop burnt down last month."
"Oh. Okay, let's work this out then..."
This is kind of the idea of debugging. You step through all the relevant steps, checking all the inputs, changes and outputs. When you find where it goes awry, you know that's where you have to fix things.

One tricky thing is that a heck of a lot of things happen every turn in TADS 3. You don't want to go through the laborious task of stepping through code for every command you run, nor do you want to run through all the code - you just want to test a certain thing. Debuggers allow you to set what is called a breakpoint. This is where you want to start debugging from. This can be set to almost anywhere that is running code. We'll look at them in more detail later on.

Now you might be thinking, "Okay, so the compiler takes my code and turns it into binary. If I'm debugging live code, does that mean I'm dealing with the binary?" You may have not noticed the option, but in TADS 3 Workbench under the Build menu there are three main compiling commands: Compile and Run, Compile for Debugging and Compile for Release. In programming languages like C or C++, the idea is that when you're creating the program you compile for debugging mode which lets you debug your software. When it is ready to give out the world, they don't need the extra guff that debugging requires, so you compile for "release". You can't debug a program compiled for Release, because it omits the information the debugger needs. By default, "Compile and Run" compiles for debugging so you can run your game and debug it. If you happen to release a game that was compiled in Debug mode, nothing bad could really come from it. People might be able to poke through the file for cheaty information, but they'd need your source code for the whole story. The only big issue is that the debug mode t3 file is bigger than the release mode one.

By default we'll be compiling everything for Debug mode, so you can use that command to compile your code, or Compile and Run. Doing this will let the compiler keep a bunch of bookmarks and hints in the binary for it to be able to reference your source code. You don't need to do anything fancy to your source code, just have it around to read.

Diving in

Let's have a go at debugging. We'll open up the sample game I wrote and go from there. To do this:

Have a look around the file to get your bearings but don't edit anything yet. Let's see what's in store.

Press "Compile and Run". You should get a screen like this.

Oh no! This doesn't look good. If you try a few standard commands (like x me ) you'll find that the game isn't broken, it's just in some weird state. Time to try our first debugging experience!

Switch to the TADS 3 Workbench and press the "Break into Debugger" button (it looks like a pause button). You should have a file called input.t pop up, and a little yellow arrow will be pointing to a line near something saying getInput: (if you don't get exactly what I get, don't worry, it won't matter). Let's explain this to get our bearings in computerland. If you've worked with IF, you expect the computer to do nothing while you've got a command line. You press enter, stuff happens and it goes back to waiting. This is more or less what TADS 3 does, but because it has capabilities to do stuff in real-time when it looks like it's waiting for input it's actually buzzing through a bunch of code. Some of this code is "wait for the player to input something". When we've jumped into the debugger, we've caught the computer in the middle of what it was doing, which was waiting for input. Exactly where we've caught the computer depends on when we pressed the "Break into Debugger" mode. In other words the computer has been going "waitingwaitingwaitingwaiting" and we've said, "Stop! What are you doing?" and bringing up input.t and pointing to a line in the code is its answer.

We might be feeling squeamish about having stopped everything, so we can let the program resume what it's doing by pressing the "Go" button (it looks like a "Play" button - get the analogy between video playback?). When you do, TADS 3 might say "Beg your pardon?" I haven't pinpointed exactly why it does this, but I think jumping out of the debugger is the same as entering an empty command, hence the response. It doesn't do anything untoward to your game, so don't worry.

Back to the subject at hand, we want to know why the TADS 3 world is empty and weird instead of showing us the starting room that we know is in the source code. So Break Into Debugger again ("Pause"). Close input.t because the problem isn't there.

When we're in the debug mode, the entire game is frozen in time like the Lady in the Red Dress scene in The Matrix. We can take our time and poke around the code and data to see what's going on. If the program hit an error, the debugger would automatically stop the game on the command that caused the error. We'll look at an example of that later on, but at this point, the game is doing something we don't expect, but not wrong in terms of confusing the computer. This distinction is useful to keep in mind: there are bugs that the computer will choke on (like dividing by zero or changing the name of nil ) but there are bugs that the computer will merrily continue with but looks wrong to your human eyes. This case is the latter. The computer will take in our commands and give responses, but the situation seems off somehow.

Our toolkit

Let's look at the tools that help us debug. If you can't see them, go to the View menu and the item of interest. Some people turn bits on and off, depending on how they like to work.

Call Stack

Code is typically separated into different functions. Inside a function, code may call another function which does stuff, then returns a value or at least brings execution back to the original function. The program needs to keep track of where it is in all this mess, which is the "Call Stack". My call stack looks like this:

inputManager.getInputLineExt(obj#2ef0 (BasicInputDef))+ 46
inputManager.getInputLine(true, obj#2ef1 (AnonFuncPtr))+ 1b
readMainCommand(rmcCommand)+ 32
readMainCommandTokens(rmcCommand)+ 12
guy.executeActorTurn()+ 97
{anonfn:2ed5}()+ 16
senseContext.withSenseContext(nil, sight, obj#2ed5 (AnonFuncPtr))+ 20
callWithSenseContext(nil, sight, obj#2ed5 (AnonFuncPtr))+ 18
{anonfn:2eda}()+ 3f
withCommandTranscript(CommandTranscript, obj#2eda (AnonFuncPtr))+ 4e
withActionEnv(EventAction, guy, obj#2eda (AnonFuncPtr))+ 3f
guy.executeTurn()+ 31
runScheduler()+ d7
runGame(true)+ 31
gameMain.newGame()+ 22
main(['debug\\tutorial.t3'])+ 17
_mainCommon(['debug\\tutorial.t3'], nil)+ 6b
_main['debug\\tutorial.t3']
Quite a mess, isn't it? The Call Stack is a stack, which means something in computer science. Just think of it as an in-tray. The very first thing you put in is on the bottom. New things go on top and in this case, we deal with the thing on the top first. At the very bottom you can see _main which is where all TADS 3 programs start. Up a bit you can see gameMain.newGame() which means to start a new game. There's a whole bunch of technical stuff in the middle and at the top you can see stuff related to input. The line pointed to by the yellow arrow will be in the function on the top of the call stack.

Why would we care about this? Well, suppose a problem starts earlier in the code (opening a door accidentally refers to the wrong door). We can see what is being run and on what, so we can see how we got to our current mess. You'll notice in the above call stack there are references to guy , which is our initial player character. So we know that we are being assigned to the right character, but something is not quite right.

You can double-click on the functions in the call stack and TADS 3 workbench will show you the associated code. It'll also set up all the variables to refer to the current values (from the perspective of that function). Double-click on guy.executeActorTurn() . This takes you to some code in actor.t . You can browse the code to see what's happening beforehand. Hover the mouse over pendingCommand . This tells you the current value of pendingCommand (which should be some "obj#2fbd (Vector)" or something like that). If you hover over something like isPlayerChar() , you'll get no love. We can show how to get these values next.

Nothing seems obviously awry from here, so we'll continue on.

Local Variables

Hovering the mouse over every little variable might be annoying, so we need a way to at-a-glance see all the local variables, that is, all the variables defined as local in our current function. This is accessed through View Local Variables. It'll pop up a little list of variables that you can inspect. Make sure the first column is wide enough to show you the variable names.

In the screenshot we can see two variables for my currently highlighted function guy.executeActorTurn()+ 97 : self and toks . (I know it's that function because of the Call Stack) Recall that "self" is the current object that is having code executed on it, in this case guy . toks is nil, which is unsurprising as we're not doing any actual command. If you look at self you'll notice that it has a little plus sign (+) next to it. Click that.

Since guy is an object, it'll have a whole bunch of data attached to it. There's a few pages of data attached to guy from temporary variables used for sense calculations to vocabWords . Each of those might have a plus sign underneath so you can start to explore those objects and so on. Don't get lost down the rabbit-hole - we're trying to debug why we're getting nothing on the game screen. At the moment there are way too many variables to inspect individually to try to figure out what's going on, so we might want to limit our scope.

Watch Expressions

Watch Expressions (accessible by View → Watch Expressions) are like the Local Variables browser, but much more customizable. Moreover it lets you look at anything - it doesn't have to be a local variable. Literally a Watch Expression is an "expression that you watch". An expression is any single line of code like an object ( guy ), a property ( guy.vocabWords ), a function ( guy.isPlayerChar() ) or a little equation like treasureChest.numberOfKeys() + 1 > 0 You can define some code and get the debugger to show you the value at all times. This is a heck of a better way of operating than a bunch of say commands.

Putting our detective hats on, we'll define a bunch of expressions that we think might point to the culprit. If we play the game a little and experiment with basic commands (try examine me , north and look ) we find that we're in some weird room where we can't examine ourselves because it's too dark, can't go north even though we expect to (since startRoom goes north to otherRoom ), and look ing says "Nothing obvious happens". We're going to set up two watch expressions to try to figure out where guy is.

You'll now notice the watch expressions have values next to them. Mine say: startRoom.contents [defaultFloor,defaultCeiling,defaultNorthWall,defaultSouthWall,defaultEastWall,defaultWestWall] guy.location nil

Okay, so the startRoom contains a bunch of default walls, floor and ceiling, but no guy . So he isn't in the startRoom . If we look to the next line, his location is... nil ?! Remember that nil isn't an actual object or room, but is the same as "nothing" or "false". It's also the default value for any property. So we can interpret this as " guy is located at nowhere". He's in the equivalent of the the abyss. TADS 3 is neat that it can handle an obvious weirdness like this and chug along. There are reasons for this, but it's outside the scope of this tutorial.

So we know that guy is not located anywhere. Let's inspect the source code to see what's going on.

startRoom: Room 'Start Room'
    "This is the starting room. "
    north = otherRoom
;

guy: Actor
;

Awwwwwwwwww nuts. I forgot to put in the + sign before guy, which sets his location as startRoom. If I wanted to do it explicitly, I should have added a location = startRoom property to him. Let's fix the source code:

startRoom: Room 'Start Room'
    "This is the starting room. "
    north = otherRoom
;

+ guy: Actor
;

Kill the debugging session ("Terminate Program"), recompile and run ("Compile and Run"). If you've fixed the code correctly, we should now get a much more useful start to our game.

Evaluate

There is another way to get the power of Watch Expressions, but designed for one-off use. This is the "Evaluate" option, found under Debug → Evaluate..., or Ctrl-E, or the little magnifying glass icon on the toolbar. It's only available when you're in debugging mode (because you need to refer to values in a running game). Try it now:

  1. Click "Evaluate" on the toolbar.
  2. Type in guy into the editing box.
  3. You can now explore all the properties of guy. Look at curState, his current state. If you click on this property it'll expand into all the properties of his curState.

If you look at the library reference for Actor, you can find functions you can examine that aren't listed by default in the Evalute box. Take, for example, isIn( obj ) . We can run this in Evaluate and get the result:

  1. Click "Evaluate" on the toolbar if you aren't in Evaluate mode already.
  2. Type in guy.isIn( startRoom ) and press enter.

Evaluate can even be used to invoke changes outside of the usual command line. Currently there is a treasure chest in the other room that we haven't implemented a key for. We can unlock the chest, bypassing all the key stuff for now.

  1. Run the game and try opening the treasure chest to make sure you can't get in.
  2. Break into the debugger.
  3. Click "Evaluate" on the toolbar.
  4. Type in the source code name for the treasure chest (treasureChest).
  5. We could mess with the most basic code and do treasureChest.isLocked_ = nil but that might not trigger side-effects that we'd want. For example, for a door it'd only unlock one side, or potentially break the two-sided interface. So we don't do this.
  6. If we know our implementation well enough, we know that makeLocked is the thing the lock/unlock command uses to do all the bookkeeping, so we use that: In "Evaluate", type in treasureChest.makeLocked(nil).
  7. Set the game running at full speed again ("Go", not "Compile and Run" nor "Restart").
  8. Open the treasure chest. You can now open and close it how you like, but still need the key to lock it.

You can now fix this problem if you like. Add the following code:

treasureChestKey : Key
    vocabWords = 'key'
    name = 'key'
    desc = "This is a key to the treasure chest. "

    // We put this in our inventory at the start.
    location = guy
;

Also don't forget to change keyList in treasureChest to include treasureChestKey . That is, replace keyList = [ /* TODO: Make a key */ ] with keyList = [ treasureChestKey ]

Stepwise debugging

This is the real deal. Stepwise debugging lets you watch a program run step-by-step so you can figure out what's going on. We're going to debug an Oracle statue. The idea of the Oracle is to tell you useful stats about how many blocks you have found in the world. You just press a button to activate it. One of our beta-testers has said that this Oracle only sometimes works, and when it doesn't it breaks badly.

Whenever you're going to debug something, you should have a plan of attack (at least mentally). Test a few specific behaviours and compare how the object reacts versus how you expect it to. Our plan of attack will be:

  1. Get all the blocks and press the button on the Oracle. This tests the basic, expected behaviour for the maximum result it could achieve.
  2. Get one of the blocks (preferably the one in the sack) and press the button on the Oracle. This tests the basic, expected behaviour for partial results.
  3. Get no blocks and press the button on the Oracle. This tests an extreme case I might not have thought of before: pressing the button before you were ready.

Try these now to see what you get. Restart the game after every test to make sure you're not messing up your experiments.

Here's what I get when I test the first behaviour:

Start Room

This is the starting room. To the north is another room.

>i
You are carrying a key.

>n
The Other Room

This is another room entirely. To the south is the original room. The middle of the room is dominated by a treasure chest overlooked by The Oracle.

You see a sack here. The Oracle contains a button.

>unlock chest
(with the key)
Unlocked.

>open chest
Opening the treasure chest reveals a red block.

>open sack
Opening the sack reveals a blue block.

>[I've seen both blocks now.]
The story doesn't know how to use the character '[' in a command.

>push button
I, the mighty Oracle, know that you have seen 2 blocks out of a total of 2. Thus you have seen 100% of the blocks.

All cool. At this point you'll be tempted to scoff at the silly beta-tester, but you decide to humour them with the rest of the testing. Here's what happens when I try to see just the blue block.

Start Room

This is the starting room. To the north is another room.

>n
The Other Room

This is another room entirely. To the south is the original room. The middle of the room is dominated by a treasure chest overlooked by The Oracle.

You see a sack here. The Oracle contains a button.

>open sack
Opening the sack reveals a blue block.

>[I've seen just one block now.]
The story doesn't know how to use the character '[' in a command.

>push button
I, the mighty Oracle, know that you have seen 1 blocks out of a total of 1. Thus you have seen 100% of the blocks.

Hey wait! There are two blocks in the game, not just one. Something is awry but I resist debugging straight away. I've got one more test and it might help guide my debugging efforts.

Start Room

This is the starting room. To the north is another room.

>n
The Other Room

This is another room entirely. To the south is the original room. The middle of the room is dominated by a treasure chest overlooked by The Oracle.

You see a sack here. The Oracle contains a button.

>[I've not seen any blocks at all.]
The story doesn't know how to use the character '[' in a command.

>push button

At this point, the game explodes with a message "division by zero". What the? From here here's how we debug it.

  1. Press "OK" or the enter button to dismiss the "division by zero" error.
  2. TADS 3 Workbench will be opened to tutorial.t on line 120. We see a division here, so this is where things have gone wrong.
  3. Hover the mouse (or use the Evaluate button) to get the current value of seen and blocks. The former is zero (as expected because I didn't see any blocks). The latter is also zero... for some reason.
  4. I look up and see a bunch of blocks++ lines, so what the heck?
  5. Determined to watch this process in action, I set a breakpoint at the start of the function. Remember a breakpoint stops a debugger when it gets to it. I can set them on a line of code by left-clicking on the grey margin on the left. A little red dot should appear on that line of code.
  6. I restart the game either by Debug → Restart game, or "Terminate program" and "Go".
  7. I run through the exact same commands and instead of dying at the "push button" stage, it breaks into the debugger.
  8. The debugger is stopped on the line local seen = 0. If you hover over seen you'll notice that it has a value of nil. This is because this line hasn't been run yet, so seen has no value yet.
  9. I click "Step Into" twice and make sure the values seen and blocks are set to zero. So that's okay.
  10. We are now on if( blueBlock.seen == true ) {. If we hover over the first bit blueBlock.seen will be nil as expected as we haven't seen it.
  11. We click "Step Into" to go to the next line of code. It's skipped to the similar line for redBlock. It has also not been seen.
  12. We click "Step Into" and it skips down to the percentage line. We stop to think about this.
  13. seen has the value we expected, but blocks is never touched. Aha! The blocks++ shouldn't be inside the if statement because it gets skipped if you haven't seen the block!

If we move the blocks++ to outside of the if statements and re-run our tests, they all give the expected results. Oh man, that was an embarrassing bunch of errors. Lucky we have beta-testers and debuggers!

More techniques

So we've seen a bunch of the debugger tools and used stepwise debugging to solve a weird behavioural bug and a catastrophic "divide by zero" bug. There are a few more little things we have at our disposal that I'll finish up on.

During stepwise debugging we have the choice of "Step Into", "Step Over" and "Step Out". If you want to follow every little step in debugging, use "Step Into". Any basic steps will just execute and if we come across functions, you'll dive into them. Sometimes functions are obviously not related to your issue and you just want to skip it (especially for some of the complicated sensory code). This is what "Step Over" does. If you are on a function, it will run all the code in that function in one hit, without you having to click through every step. If you find yourself in a function and you just want the rest of the code in that function to execute, run "Step Out". If you want to fast-forward to a specific bit of code, click your cursor in the code and use "Run to cursor". In summary:

Note that these commands will stop if it hits a breakpoint or fatal error.

Sometimes you know that a bunch of lines are troublesome and you want to skip them. You can click on a line (setting the cursor) and then click "Set Next Line". Note that this has the nifty ability to move to code that's already been executed. It won't rewind any changes you've done, however.

You can set as many breakpoints through your code as you like. Whenever you run the debugger, it'll stop at those lines. You can review the breakpoints via Debug → Edit Breakpoints. This is handy place to get rid of all the breakpoints.

Wrap-up

We've had a go at all the different debugging capabilities in TADS 3. The only thing now you need is practice. Lots of practice. Practice reading code and sanity-checking it. Practice following functions through the library and knowing what class is responsible for what. Practice setting tests to catch bad behaviour and implementing those tests. Learn to avoid say("Got here!"); shenanagins. Most of all, don't be afraid to ask questions and learn from the friendly TADS 3 community.