Debugging TADS3 on Linux

Howdy,
I downloaded and built frobtads on debian, compile TADS3 projects using t3make and use gargoyle-free to run my apps (as frob does not support german character set out of the box).
Does anyone have experience on how to debug a TADS3 app on linux? Is there a native debugger that can be built from source? What other strategies do people use?

I am having issues I can only understand (and hopefully fix) while debugging I fear. (Un)fortunately, I don’t have a Windows machine at home so I am a bit lost here :confused:

Happy to hear from you, best - fx

3 Likes

A suboptimal solution I found is that the TADS 3 workbench works pretty well via Wine. There are a bunch of caveats: if you’re like me and compile TADS 3 via Makefiles, importing into workbench will mess everything up.

For Hand Me Down I had a debug make target that copied the source directory to a debug area, and run workbench in Wine from there with custom debug build environment.

3 Likes

I use a set of debug routines, some I have shared in this forum (e.g. a “scroll of knowledge” when read, prints the current values of the variables & flags investigated)

Also I use “Imp’s gates” for quickly reaching far points in the map; in general, I don’t feel the need of debuggers when I can create adhoc debug commands/items.

Best regards from Italy,
dott. Piergiorgio.

3 Likes

A while back I wrote a simple interactive debugger for TADS3/adv3 (there’s also an adv3lite version which I think works the same but I don’t use adv3lite so ymmv). It’s “just” TADS3 code itself, so it’s not a true debugger; it just uses the builtin reflection services and exception handlers to drop execution into a very simple bespoke debugging command parser and expression evaluator. It’s not much, but it does let you set breakpoints and/or drop into the debugger to look at the contents of the stack when the game barfs.

I do my TADS3 coding mostly on linux as well, and my debugging process is mostly a combination of using that and simple debug-by-printf()-ish stuff.

I’ve also written a very simple linter module for implementing linters, but that’s more for commemorating rules for static analysis that you yourself (as the game designer) come up with than doing “generic” debugging.

5 Likes

(Also a linux dev) If you haven’t tumbled onto it yet, add -source reflect to your debug builds. This gives you access to VERY enhanced stack trace info, as well as access to the reflectionServices.valToSymbol() which dumps symbol information as printable strings (instead of needing to suss out string properties of an unknown object). Then yeah, lots of debug prints. I crafted a bare bones debug ‘module’ to give me a consolidated space to put that stuff, though inevitably some debugs require in-method print statements anyway.

#charset "us-ascii"

/*   Debug Printer...
 *
 *   Include the main header for the standard TADS 3 adventure library.
 *   Also include the US English definitions, since this game is written
 *   in English.  
 */
#include <adv3.h>
#include <en_us.h>

// some convenience debug macros.  need to define in any source
// file that wants to use them, though only v2s() likely useful
//
#define gDebug (debugMgr.debugEnabled)
#define gQuiet (debugMgr.quietEnabled)
#define v2s(Symbol) reflectionServices.valToSymbol(##Symbol)

//  QUIET function, mainly used to shut off output when
//  'FF' to debug areas deep in gamestate
// ie..  >QUIET
//       >REPLAY DEBUG.TXT
//       >QUIET
//
DefineSystemAction(Quiet)
    execSystemAction() {
		gQuiet = (gQuiet ? nil : true);  // toggle value
        if (gQuiet) {
            "##QUIET MODE ENABLED\n";
            mainOutputStream.disable();
        } else {
            mainOutputStream.enable();
            "##QUIET MODE DISABLED\n";
        }
    }
;
VerbRule(Quiet)
	('quiet') : QuietAction
	verbPhrase = 'quiet/quieting'
;
modify mainOutputStream
    outputOn = true
    enable() { outputOn = true; }
    disable() {outputOn = nil; }
    writeFromStream(txt) {
        /* if an input event was interrupted, cancel the event */
        inputManager.inputEventEnd();

        /* write the text to the console */
        if (outputOn) aioSay(txt);
    }
;

//  DEBUG function, enables every turn print of desired values
//
modify DebugAction
	execAction() {
		gDebug = (gDebug ? nil : true);
        "##DEBUG MESSAGES <<gDebug ? 'ENABLED' : 'DISABLED'>>";
        if (gDebug) new Daemon(debugMgr, &debugPrint, 1);
        else eventManager.removeMatchingEvents(debugMgr, &debugPrint);

        /* ORIG:  if the debugger is present, break into it,
         * never played with this, sorry
         * 
        if (t3DebugTrace(T3DebugCheck))
            t3DebugTrace(T3DebugBreak);
        else
            "Debugger not present. "; */
	}
;

debugMgr : Thing
    debugEnabled = nil
    quietEnabled = nil
    debugPrint() {
        // debug prints go here

        "DEBUG:  bigSword.location = <<v2s(bigSword.location)>>\n";
    }
;

Would also recommend this thread for stack trace chasing hints…

7 Likes

Yeah, for “just” debug-by-printf() stuff I also wrote the syslog module, which defines a Syslog class you can use as a mixin.

It provides a _debug() method. The simplest usage is a single argument, a text literal. It’s output only if the game is compiled with -D SYSLOG.

You can also provide a second optional argument as a logging tag/ID. Logging tags can be turned on and off via syslog.enable(id) and syslog.disable(id).

All instances of Syslog can also optionally define a syslogID property. If defined, it will be prepended onto each line of debugging output.

3 Likes

This looks promising. After some first testing I got a little bit of a grasp here… I had to define a verb rule for “debug”, which then toggles debugging as expected, but the debug messages only get printed ONCE after I toggle debugging on (and not after every action the player performs). This is fixed by removing the comment in the DebugAction that starts with ORIG: …
This is a great start to built on top of. I will look closer into reflections and the debugMgr in the near future. You would not have any idea how to set a breakpoint and step into executed lines of code one after the other, would you? :slight_smile:
Thank you for the example code !

2 Likes

That is helpful, and I will look into it, thank you

1 Like

Sorry, no, I have not tried that. Good luck though, and welcome!

1 Like

Brief lull in my WIP, taking the opportunity to update my debug module. There is a lot of code here for some pretty modest (but useful!) functionality additions. Note this builds on the work discussed here. Specifically, because of an apparent bug in frobtads, there are more gymnastics here than usual to get dynamic code to work. Seems to be pretty cross-platform at this point, tested under Lectrote, Gargoyle, QTADS, Frobtads and Parchment.

Previous functionality:
QUIET mode, to disable/enable output, mostly used when processing transcript to advance gamestate to debug point
DEBUG mode, to report ‘spy list’ values every turn

Additional functionality (work independent of DEBUG mode)
Peek (DX) - display property and some method values
Poke(DS) - set property values, not tested for complicated methods
(supports both >ds obj.property and >ds obj.property = val syntax. Former case will prompt for val)
Spyadd (DA) - add property to ‘spy’ list of reported properties echoed every turn
Spydel (DD) - remove property from same list

Note Spy list can ALSO be seeded directly in code, as might be more convenient for a drawn out debug to play with a static list for a while. DA/DD will append to or prune list, including initial seed.

Included is a string preProcessor that allows you to omit quotes around the symbols in question.

#charset "us-ascii"

/*   Super Basic Debug Manager...
 *
 *   Include the main header for the standard TADS 3 adventure library.
 *   Also include the US English definitions, since this game is written
 *   in English.  
 */
#include <adv3.h>
#include <en_us.h>
#include <dynfunc.h>
#include <tads.h>

//  GENERAL MACRO DEFs, opportunistically for pasting to other files
#define gDebug (debugMgr.debugEnabled)
#define gQuiet (debugMgr.quietEnabled)
#define v2s(Val) reflectionServices.valToSymbol(Val)

/*
 *  Macro Defs for readability
 *
 *  DEBUG EXAMINE:  dx(property) :  JZ workaround to frobs bug, unclear why needed!
 *
 *  SPYLIST SHORTHAND:  spy(property) : adds to lookup list, in way that facilitates frequent edits
 */
// thanks JZ!
#define dx(val) { \
   local str = '\n'## #@val ## ' = <<reflectionServices.valToSymbol(val)>> \n'; \
   if(gTranscript) \
      extraReport(str); \
   else say(str); }
#define spy(val) ''## #@val ##'' -> function() { return reflectionServices.valToSymbol(val); }

/*
 *  DEBUG VERBS
 *
 *  QUIET:  disables output, intended for use when importing transcript to FF gamestate to debug area
 *
 *  SPYADD/SPYDEL:  adds/deletes properties from spy list - list of signals reported every turn when DEBUG mode enabled
 *      [shortcuts are da/dd]
 *
 *  PEEK:  report current value of any property [shortcut dx]
 *
 *  POKE:  deposit value in any property [shortcut ds]
 *
 *  DEBUG:  enter debug mode, really just turn on SPYLIST reporting
 *
 *  Most make use of DynFunc capabilities to compile string variables as executable code.
 *  Note Frobtads bug prevents use of Compiler.eval()
 */
//  Adding output disable capability, specifically for debug FFW
//
modify mainOutputStream
    outputOn = true
    enable() { outputOn = true; }
    disable() {outputOn = nil; }
    writeFromStream(txt) {
        /* if an input event was interrupted, cancel the event */
        inputManager.inputEventEnd();

        /* write the text to the console */
        if (outputOn) aioSay(txt);
    }
;
DefineSystemAction(Quiet)
    execSystemAction() {
		gQuiet = (gQuiet ? nil : true);
        if (gQuiet) {
            "##QUIET MODE ENABLED\n";
            mainOutputStream.disable();
        } else {
            mainOutputStream.enable();
            "##QUIET MODE DISABLED\n";
        }
    }
    actionTime = 0
;
VerbRule(Quiet)
	('quiet') : QuietAction
	verbPhrase = 'quiet/quieting'
;
//======================= SPY ADD ========================
//
DefineLiteralAction(SpyAdd) 
    execAction() {
        Compiler.compile('function() { debugMgr.spyProps[\'' + gLiteral + '\'] = new function() { return reflectionServices.valToSymbol(' + gLiteral + '); }; }')();
        debugMgr.debugPrint();
    }
    // this is a meta-command, so don't consume any time
    actionTime = 0
;
VerbRule(SpyAdd)
	('spyadd' | 'da') singleLiteral  : SpyAddAction
	verbPhrase = 'spy add/adding (what)'
;
//======================= SPY DEL ========================
//
DefineLiteralAction(SpyDel) 
    execAction() {
        Compiler.compile('function() { debugMgr.spyProps.removeElement(\'' + gLiteral + '\'); }')();
        debugMgr.debugPrint();
    }
    // this is a meta-command, so don't consume any time
    actionTime = 0
;
VerbRule(SpyDel)
	('spydel' | 'dd') singleLiteral  : SpyDelAction
	verbPhrase = 'spy delete/deleting (what)'
;
//=======================  PEEK   ========================
//
DefineLiteralAction(Peek) 
    // via JZ...
    execAction() {
        Compiler.compile('function() { dx(' + gLiteral + '); }')();
    }
    // this is a meta-command, so don't consume any time
    actionTime = 0
;
VerbRule(Peek)
	('peek' | 'dx') singleLiteral  : PeekAction
	verbPhrase = 'peek/peeking (into what)'
;
//=======================  POKE   ========================
//
DefineLiteralAction(Poke) 
    execAction() {
        // local syntaxOk = true;
        if (debugMgr.pokeVal == nil) {
            "DEBUG:  enter value >";
            debugMgr.pokeVal = inputManager.getInputLine(nil, nil);
        }
        Compiler.compile('function() { ' + gLiteral + ' = ' + debugMgr.pokeVal + '; }')();
        nestedAction(Peek, gLiteral);

        // Abandoned syntax checking attempt
        //
        // local setFn = Compiler.compile('function() { ' + gLiteral + ' = ' + val + '; }');
        /* try { setFn(); }
        catch(Exception err) { "DEBUG: <<err.exceptionMessage>>\n"; syntaxOk = nil; }
        finally {
            if (syntaxOk) {
                setFn();
                "<<gLiteral>> set to <<val>>";
            }
        } */
    }
    // this is a meta-command, so don't consume any time
    actionTime = 0
;
VerbRule(Poke)
	('poke' | 'ds' ) singleLiteral : PokeAction
	verbPhrase = 'poke/poking (into what)'
;
//=======================  DEBUG  ========================
//
modify DebugAction
	execAction() {
		gDebug = (gDebug ? nil : true);
        "##DEBUG MESSAGES <<gDebug ? 'ENABLED' : 'DISABLED'>>";
        if (gDebug) new Daemon(debugMgr, &debugPrint, 1);
        else eventManager.removeMatchingEvents(debugMgr, &debugPrint);

        /* ORIG:  if the debugger is present, break into it 
        if (t3DebugTrace(T3DebugCheck))
            t3DebugTrace(T3DebugBreak);
        else
            "Debugger not present. "; */
	}
;
/*
 *  DEBUG MGR
 *      manages quiet/debug states and debugPrint method reports SPYLIST every turn
 *      can either pre-define with spy(prop) macros, or add/del at runtime
 *      also stores set value for poke command, as needed
 *
 *  Note Spylist is a lookup table where entries are of format 'property-as-string'-> anonFn() { return v2s(property); }
 *  this was needed to ensure current values always reported, not just value at time of adding property to list
 *  spy(prop) macro just makes it easier to add properties offline
 *
 */
debugMgr : object
    debugEnabled = nil
    quietEnabled = nil
    pokeVal = nil // value to set property to for ds command

    // compile-time spy list, da adds to this, dd removes
    spyProps = [spy(me.location)]
    debugPrint() {
        if (spyProps.keysToList().length() < 1)
            "DEBUG_ERR:  NO PROPERTIES SPIED!  Add with \'>spyadd [prop]\'";
        else {
            "<.p>";
            spyProps.forEachAssoc(function(key, val) {
                "DEBUG:  <<key>> = <<spyProps[key]()>>\n";
            });
        }
    }
;
/*
 *  PREPARSE command string to allow Literal input without
 *  surrounding quotes.  Prevents parser tripping over periods
 */
StringPreParser
	doParsing(str, which) {
        local pokeRegex = R'^(<^Space|=>+)<Space>*=<Space>*(.*)$';
        local dbgRegex = R'<NoCase>^<Space>*(peek|dx|spyadd|da|poke|ds|spydel|dd)<Space>+(.*)$';
        local trailSpaceRegex = R'(.*<^Space>)<Space>+$';
        local quoteLeadRegex = R'^(\'|\")';
        local quoteTrailRegex = R'(.*)(\'|\")<Space>*$';
        local leadQuote = '\'';

        local propStr = '';
        local cmd = '';

		if (rexMatch(dbgRegex, str)) {
            propStr = rexGroup(2)[3];
            cmd = rexGroup(1)[3];
            debugMgr.pokeVal = nil;
            if ((cmd is in ('ds', 'poke')) && (rexMatch(pokeRegex, propStr))) {
                propStr = rexGroup(1)[3];
                local pokeVal = rexGroup(2)[3];
                if (rexMatch(quoteTrailRegex, pokeVal)) pokeVal = rexGroup(1)[3]; // trim trailing quote
                if (rexMatch(trailSpaceRegex, pokeVal)) pokeVal = rexGroup(1)[3]; // trim trailing spaces
                debugMgr.pokeVal = pokeVal;  // save set value in Mgr
            }
            if (rexMatch(trailSpaceRegex, propStr)) propStr = rexGroup(1)[3]; // trim trailing spaces
            if (!rexMatch(quoteLeadRegex, propStr)) propStr = leadQuote + propStr; // add leading quote if missing
            else leadQuote = rexGroup(1)[3];  // in case lead with \"
            if (!rexMatch(quoteTrailRegex, propStr)) propStr = propStr + leadQuote; // add trail quote if missing
            str = cmd + ' ' + propStr;
        }
        return str;
    }
;

This lets you do the following:

>dx me.age18plus
me.age18plus = nil

>ds me.age18plus
DEBUG: enter value >true
me.age18plus = true

>ds me.age18plus = nil
me.age18plus = nil

>da me.age18plus
DEBUG: me.location = startRoom
DEBUG: me.age18plus = nil

>dd me.location
DEBUG: me.age18plus = nil

QUIET and DEBUG work as before. I use these capabilities A LOT.

3 Likes

Interesting that we arrive at such similar debugging methods in isolation…
I never got to the point of adding props to the “spyList” in-game, so that’s a cool touch…

2 Likes

@jjmcc, this looks totally awesome. Does this work with ADV3Lite, by any chance?

2 Likes

I think it would need some adaptation to work with adv3Lite, not least because adv3Lite doesn’t use a transcript, but adv3Lite already has its own set of debugging commands, including EVAL which can be followed by any expression or command you like. For example:

EVAL me.name would display the name of the me object.

EVAL banana.bulk = 2 would set the bulk property of banana to 2

But it may be this won’t work on FrobTADS due to the bug I’ve seen mentioned about it upthread?

3 Likes

Never tried, but Eric undoubtedly would know better than me. Certainly command-line EVAL as a built-in capability solves 90% of the adv3 gymnastics (such as they were). Just curious (ahead of my inevitable migration), does lite have an analogous valToSymbol capability too?

Effectively, yes. The adv3Lite file debug.t uses t3GetGlobalSymbols() to set up a symbol table, which is then used to define symToVal() and valToSym() functions, which are available when compiling for debugging.

I’m just looking at debug.t to see what might be added in light of this thread.

EDIT: I’ve just changed the way adv3Lite’s EVAL command work so that it uses Compiler.eval() rather than Compiler.compile(), in the hope that that will circumvent the problem with Compiler.eval() in FrobTADS.

3 Likes

I admit that a forthnight or so ago I have resorted to a debug build, for an annoying crash, and indeed the debug crash message (under QTads) immediately pointed to the exact line where the bug lies; first time in my IF coding when my set debug items failed… but having recovered from the bakups that debug build, I have played a bit with EVAL and is indeed interesting; but as people whose/which have seen my “Scroll of State” will get my drift, I think that EVAL var, (var,)… can be very useful.
(perhaps something like EVAL as a method (e.g.setting a var/flag if another has a specific value) can be useful ?)

Best regards from Italy,
dott. Piergiorgio.

1 Like

If only QTads was accessible to screen reader users… It’s far more likely that it would be accessible on Linux, as Orca and QT can communicate through the QT-ATSPI library, but on Windws and Mac… Well, that’s a whole different discussion. If @RealNC wants to discuss that at some point, then I’m more than willing to.

Back to the topic at hand, I had no idea ADV3Lite had its own expression evaluator, but that’s nice to know.

1 Like