nil object reference. Parser bug? TADS3

A tester typed, >Sure thing, honey, and he got the msg [Runtime error: nil object reference]
(None of these words are used in the game)
When I do this in the debugger, the game drops into Parser.t with a yellow pointer to the line below which reads:

if (ret.length() == 0)
                results.noVocabMatch(resolver.getAction(), getOrigText());

More generally, any time two unknown words are entered, separate by a comma, this bug occurs. For example:

abcde, fghij
will produce the error as well.

The code below is from parser.t, and can be found under:
class NounPhraseWithVocab: NounPhraseProd

    resolveNounsMatchName(results, resolver, matchList)
    {
        local origTokens;
        local adjustedTokens;
        local objVec;
        local ret;

        /* get the original token list for the command */
        origTokens = getOrigTokenList();

        /* get the adjusted token list for the command */
        adjustedTokens = getAdjustedTokens();

        /* set up to receive about the same number of results as inputs */
        objVec = new Vector(matchList.length());

        /* consider each preliminary match */
        foreach (local cur in matchList)
        {
            /* ask this object if it wants to be included */
            local newObj = resolver.matchName(
                cur.obj_, origTokens, adjustedTokens);

            /* check the result */
            if (newObj == nil)
            {
                /* 
                 *   it's nil - this means it's not a match for the name
                 *   after all, so leave it out of the results 
                 */
            }
            else if (newObj.ofKind(Collection))
            {
                /* 
                 *   it's a collection of some kind - add each element to
                 *   the result list, using the same flags as the original 
                 */
                foreach (local curObj in newObj)
                    objVec.append(new ResolveInfo(curObj, cur.flags_, self));
            }
            else
            {
                /* 
                 *   it's a single object - add it ito the result list,
                 *   using the same flags as the original 
                 */
                objVec.append(new ResolveInfo(newObj, cur.flags_, self));
            }
        }

        /* convert the result vector to a list */
        ret = objVec.toList();

        /* if our list is empty, note it in the results */
        if (ret.length() == 0)
        {
            /* 
             *   If the adjusted token list contains any tokens of type
             *   "miscWord", send the phrase to the results object for
             *   further consideration.  
             */
            if (adjustedTokens.indexOf(&miscWord) != nil)
            {
                /* 
                 *   we have miscWord tokens, so this is a miscWordList
                 *   match - let the results object process it specially.  
                 */
                ret = results.unknownNounPhrase(self, resolver);
            }

            /* 
             *   if the list is empty, note that we have a noun phrase
             *   whose vocabulary words don't match anything in the game 
             */
            if (ret.length() == 0)
                results.noVocabMatch(resolver.getAction(), getOrigText());
        }

Any suggestions?

Thanks,

–Bob

Yes, I recognise this - it’s a known TADS 3 parser bug. Details and fix for it on the bug database here: bugdb.tads.org/view.php?id=228&nbn=2#bugnotes

Although the provided fix will work, it’s best to not re-implement anything if you can avoid it. You can fix the problem by putting this code somewhere with your other sources (I recommend a file called “adv3libfixes.t” or similar for code that fixes or works around library bugs.)

modify NounPhraseWithVocab
{
    resolveNounsMatchName(results, resolver, matchList)
    {
        try
        {
            return inherited(results, resolver, matchList);
        }
        catch (RuntimeError e)
        {
            // Runtime error 2203 is "nil object reference."
            if (e.errno_ == 2203) {
                results.noVocabMatch(resolver.getAction(), getOrigText());
                return [];
            }
            // It wasn't a nil object reference. Re-throw the error so that it's not lost.
            throw e;
        }
    }
}

My thanks to you both. I note that Emily’s modified NounPhraseWithVocab is much longer than the one by RealNC, above. Which one should I use?

Thanks also for the advice on separating the code from the engine files (which I presume can get overwritten in an update).

–Bob

Both work.

The first one overrides the method completely. It’s a copy&paste from the library code with two new lines added in that fix the issue.

The second one calls the existing method, and if there’s a nil object reference error, it will correct the result. I posted it mostly as an example of how to catch runtime errors.

Hi RealNC.

FWIW, I copied your code in, but still got the runtime error (at least when I’m running the game in workbench)

Do I understand correctly that, if this code works correctly, I could modify it to more generally address other runtime errors should they come up, by testing the offending method to see if it is kicking out that error, and writing a suitable message?

Thanks!

–Bob

[EDIT, I should add that I subsequently copied in the fix that Emerald posted, and it does work.]

The debugger might still show the error for informational purposes. Does it still appear if you run a release build in the normal interpreter?

Yes. Exception handling can catch all VM errors, including most runtime errors. In this particular example, we know that there’s a bug in the Tads code that results in a 2203 error (the error codes are documented). We know it happens in the NounPhraseWithVocab.resolveNounsMatchName() method. So if we override that method, we can call the previous implementation (with the ‘inherited’ keyword) and watch for the error using a try-catch statement. When the error occurs, it means we’ve hit the bug and can then correct the situation. In this case, we call results.noVocabMatch(), as the original code would do (but didn’t get to, since the error was thrown) and then return an empty list. If it wasn’t the error we’re looking for, we must throw the error again since we don’t want to swallow-up unrelated errors and prevent them from showing up.

This doesn’t always work though. It depends on where the error happens in the overridden method. So you might have to re-implement the whole method anyway. In this case it was simple, since we know what the original code didn’t get around to do when the error happened. When an exception occurs, the method returns immediately at the point of the error; it does not complete its execution. In this case, it’s “ret.length()” that throws the error, since ‘ret’ is nil and the code tried to call length() on it. Anything after that was not executed because of the thrown exception.

But when in doubt, you can just copy&paste the whole thing into your override and fix the bug there. It really doesn’t matter, since in the end you will (hopefully!) remember to delete the whole override again when you install a new Tads version that has fixed that particular bug (which is the reason I recommend a libfixes.t file for that stuff, so that you don’t have to hunt down those bug-fixes all over the place.) I just like to keep things as short as possible, that’s all.

However, if you do forget to delete those fixes again later, then the second way to fix the issue (using ‘inherited’) is safer, since ‘inherited’ will call the new code, as provided by the Tads update. In that case, your fix-up code just becomes a dead wrapper method with no ill effects.

If you instead re-implement the whole thing in your override, then the overridden code (which has now been updated and fixed) doesn’t get called, which might result in new bugs, since a new Tads version might not only have fixed this one particular bug, but might also have changed the code in other ways. That new code won’t get called; only the old copy of the code will that you copy&pasted into your override.

Anyway, I’m providing way too much detail here. If you run into some Tads bug, you can just copy&paste the original code into a temporary override, fix it there, and then just remember to delete it once the bug is fixed in a future Tads update. Everything else is just me mumbling about mostly not really that important technicalities :smiley:

1 Like