Debugging TADS3 on Linux

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