Debugging infinite loops

Hi folks!

I was having some pain today trying to debug an infinite loop… hard to see what's happening when it's doing too much stuff. So I came up with this. A turn script that sets game.debug_count to zero, and then a debug function:

if (GetBoolean(game, "debug_mode")) {
  if (not HasInt(game, "debug_count")) {
    game.debug_count = 1
  }
  else if (game.debug_count > 600) {
    error ("***** DEBUG OVERFLOW *****")
  }
  else {
    game.debug_count = game.debug_count + 1
  }
  if (HasString(game, "debug_level")) {
    message = debug_level + message
  }
  msg ("<p style=\"whitespace: pre;\">DEBUG: "+message+"</p>")
}

So if one script outputs more than 400 lines of debug messages (like it's stuck in an infinite loop), it calls error.
Haven't been able to test this yet... looks like the server just went down. But while I wait, thought I'd post here; see if anyone else can point out a better way to do it, and if anyone would find it useful.


Oh, that's not useful then.
error prints a message and exits the current function.
finish doesn't do anything
Is there actually a way to force the calling script to stop? Putting while ([actual condition] and not game.debug_overflow) { on every loop works; but I was hoping it would be possible to just interrupt the game and flush remaining output.


Sounds like you need a command from Basic called stop, or end...


I was thinking of Perl's die.
I expected finish to work like that, or at least error to stop the function, but it seems errors are localised to the function, not to the running script.


if you find a way to stop/terminate/'break'/'exit' (or 'try/catch' error-n-exception-handling) scripting let us know!

as we need that to avoid crashes due to looping of getting user inputs... lol


I have a short term solution.

Use a show menu. Type "no" for the ignore thing.

The bracket numbers/places may be messed up here...

options = Split("Dun-dun", ";")
ShowMenu ("THING", options, false) {
  switch (result) {
    case ("Dun-dun") {
      msg ("You made an infinite loop.")
    }
  }
}

jmnevil: What's the point in that?

Edit: Sorry that was a bit terse.

From my understanding, ShowMenu() just displays a menu. But in this case, output seems to being buffered and never displayed (presumably because the server waits for the script to end before sending everything to the client; or maybe the client waits to receive all the data before putting it onscreen).

The "allow cancel" option just sets a flag telling the parser to ignore all other commands until the menu has been answered. That's no help when it's in an infinite loop, because the script doesn't finish.


It was just an idea for a short term solution until someone (probably The Pixie) came with better code.

Oh... so you don't want to stop the infinite code loop...? I didn't know it worked like that. (Server waits till end?) I just figured if you put it in at the right place, it would work...


Yes, I want to stop the infinite loop. But ShowMenu or show menu won't do that. It stops the user entering other commands, but it doesn't stop scripts that are already running. Think about it: code that comes after the ShowMenu (but not in the result callback) executes right away. Otherwise you couldn't do:

ShowMenu (... blah ...) {
  // do something with the result
}
msg ("(Here's some more detailed explanation of what the options mean, to help you choose)")

At least… that's what I understood from glancing through the Core functions. Am I wrong?


Have you tried using on ready?

http://docs.textadventures.co.uk/quest/scripts/on_ready.html


Nevermind. That did nothing.


The behaviour of on ready is a bit quirky. It doesn't stop anything called after it, though. If there's not a menu currently being shown or something like that, the code in it will execute immediately. If there's previously been a show menu call, the "ready" code will be delayed until the Quest engine has processed the player's response - which it won't do until the current script has finished.


In Basic, there is a command that will stop execution of the program...
^C (Ctrl C)
I doubt it is something that could be placed in a Quest script, but it could be in the Quest core code...


The only way I know to debug an infinite loop is to print a message immediately before it executes and another immediately after.
Once you strike the infinite loop, (ie. only get start message and not end message) you can then insert messages into the loop and run again (... and again ...) until you squash the bug.
However, if you are trying to trap for this in case the user does something to cause it, you would need to setup your inputs to filter out all but the correct answer(s).
I can't think of any other way. I don't think it is even possible in other languages I have used. (various BASICs, C, C++, VB, VBA, Python and others)
On the other hand, you may need to delve into the core Quest code to get to its processing levels.


Hmmm...
Maybe it was another command I was thinking...
Or maybe that was for batch files???
Oh, well...


@mrangel

on ready will wait for any blocks to complete but not scripts. Script commands in lower case, like show menu, use blocks, whilst functions in capitals, like ShowMenu, use scripts. None of which is useful here...

@R2T1

Agreed.


I say, I just delete glitchy, offending code, and start over. No need to deal with infinite loops when you can just delete the whole thing.

I also figured out how to comment on Alex W.'s blog, about this issue... so.... I don't know if he'll respond...


Oh, I found this. https://textadventures.co.uk/forum/quest/topic/ugrtq5ysmk6xnbdxkp7hxw/quest-keeps-crashing-while-i-try-to-work-solved
I don't know if it will answer your question, but it said something about infinite loops...


If you are getting infinite loops check the Boolean logic that controls the loop. Make sure that there is a proper decrement or increment variable that controls how many times it loops.

Also, make sure you don't have too many calculations that occur within that loop... as Quest doesn't like too many calculations... this can make it appear like the program has frozen, but in reality its just being excruciatingly slow. When I had just started out with quest I had made one that did like 500+ calculations within one loop, and it appeared to freeze, but after about 2 minutes of waiting, it unfroze.


"Checking the logic" is very hard if you can't get any messages out of it to find out what's going on.

The only way I know to debug an infinite loop is to print a message immediately before it executes and another immediately after.

Yes; standard debugging practice. But it's not helpful in Quest, because on the web version, all output appears to be queued up and sent as a batch when the script finishes. So if I'm in a situation where the script sometimes fails to complete, all I can know is "it didn't finish this time"; I can see what the different variables and data structures were prior to entering the loop if it works, but I can't find out anything about the dataset that caused it to get stuck.

If you really want to know what I screwed up… (now that I found it). My random maze generator looks like (pseudocode):

while (there are rooms within the container not reachable from the start point) {
  pick a reachable room A and an unreachable room B
  pick a compass direction (I'll use north in this example)
  make an exit going north from A to B
  make an exit going south from B to A
  if (A already had a north exit and B already had a south) {
    change the destination of those exits (and their reverse counterparts):
    the room that was north of A is now north of the room formerly south of B
  }
  else if (there's already a room north of A) {
    pick an unreachable room C, which doesn't have a north exit
    make north_of_A's south exit now point to C
  }
  else if (there's already a room south of B) {
    pick a reachable room C, which doesn't have a south exit
    make south_of_B's north exit now point to C
  }
}

(actually more complex than that, with more error checks against corner cases that could lead to it not terminating, but that's enough to give you some idea)

The algorithm as written looks like it could run forever; but it can't. The maths is pretty complex; but basically boils down to - the number of exits always increases or stays the same, and there's always a non-zero probability of it increasing until every room has 4 exits. Once all available exits are taken, the expectation of the change in number of reachable rooms is always positive; so as the number of iterations increases, the number of reachable rooms grows more than it falls.

The problem, then... I've got a function which changes the to of an exit, and the parent of the exit in the opposite direction. I'd messed up the logic, and assumed that GetExitByLink (x.to, x.parent) would always find an exit whose direction is the opposite of exit x. This isn't true if you're in one of the weird elf-magic loops where going north and south from a room takes you to the same place. And if it moves the wrong one, then the function responsible for "filter this list of rooms to find the ones without a north exit" starts returning a room which doesn't have a north exit, but can already be reached by going south from another room.

In this case, the loop isn't guaranteed to terminate; because the algorithm I used for determining what's "reachable" from the start room doesn't work on directed graphs. (It assumes that if I can get from A to B, I can also get from B to A).

Figuring out that whole mess without being able to dump the state of the map when it went wrong was a real pain.


Ahhh. I was assuming the game was done in the Windows desktop version... where it is significantly easier to debug infinite loops.


My new bidirectional exit model encapsulates this a whole lot better. My function MakeExits (same parameters as core CreateBiExits) now creates 2 exits in opposite directions, giving them an extra attribute, reverse linking to the other exit in the pair, and two scripts changedto and changedparent which ensure that if one exit is moved to a different room, the other comes with it. They also call a script attribute changedexits on the rooms connected by the exits, which it is useful because now my "path" and "stream" rooms can move their "path end" objects (scenery with inroomdescription) to make sure they're still in adjacent rooms when the shifting maze moves the exit around.


Log in to post a reply.

Support

Forums