Stupid (adv3) parser tricks: disambiguating between multiple noun-as-verb usages

I have a situation in which I want…or at least I think I want…to have a bare noun phrase typed as a command to be parsed as an action applying to the named object.

That’s straightforward enough to implement. If we want to create a new action, call it FooAction, and we want to have it used when the name of an object is typed by itself as a command, then all we have to do is:

DefineTAction(Foo);
VerbRule(Foo) singleDobj: FooAction verbPhrase = 'foo/fooing (what)';

Then we can just put dobjFor(Foo) handlers on any objects we want to do stuff when invoked this way, probably putting a fallback on Thing so we don’t get a “Nothing obvious happens.” by default.

Okay, so far so good. Now how about if we want to have two actions, each of which applies in different situations? Different actions for different kinds of object, for example. And what if you also want the parser to default to the normal behavior (that is, not treating noun phrases as actions) for everything except a specific class of objects?

The naive solution:

DefineTAction(Foo);
VerbRule(Foo) singleDobj: FooAction verbPhrase = 'foo/fooing (what)';

DefineTAction(Bar);
VerbRule(Bar) singleDobj: BarAction verbPhrase = 'bar/barring (what)';

pebble: Thing 'small round pebble' 'pebble' "A small, round pebble. "
        dobjFor(Foo) {
                verify() { nonObvious; }
                action() { "You foo the pebble. "; }
        }
;
rock: Thing 'ordinary rock' 'rock' "An ordinary rock. "
        dobjFor(Bar) {
                verify() { nonObvious; }
                action() { "You bar the rock. "; }
        }
;
+stone: Thing 'nondescript stone' 'stone' "A nondescript stone. ";

…doesn’t work: >PEBBLE, >ROCK, and >STONE will all be interpreted as calling FooAction.

This turns out to be a somewhat complicated problem. I’m not entirely convinced that I have the optimal solution, but I have something that works without having to hammer too much on the adv3 parser.

The trick, if you want to call it that, is to modify each action’s resolveNouns() method to mark noteWeakPhrasing() on the results object. So if we want FooAction to only apply to instances of the class Pebble:

DefineTAction(Foo);
VerbRule(Foo) singleDobj: FooAction verbPhrase = 'foo/fooing (what)'
        resolveNouns(srcActor, dstActor, results) {
                local r;

                inherited(srcActor, dstActor, results);

                if(dobjList_ == nil) 
                        return;

                r = nil;
                dobjList_.forEach(function(o) {
                        if(o.obj_ && o.obj_.ofKind(Pebble))
                                r = true;
                });

                if(r != true)
                        results.noteWeakPhrasing(100);
        }
;

The important bits to note are that you have to call inherited() first, because that’s where dobjList_ gets populated. We then just test that it contains an instance of the class we care about, and if it doesn’t we call noteWeakPhrasing() on the results object.

That gets us part of the way there. If we do this on all of our noun-as-verb actions, then this will work to disambiguate between them. Pebble instance names typed on the command line will always be handled with FooAction, for example. We could do something similar for Rock instances and BarAction. But then if we have a Stone that’s not a Pebble or a Rock, then it will end up handled by whichever noun-as-verb action happened to be declared last. Which is probably not what we want (it’s not what I want, anyway).

The trick here is that we can define an additional noun-as-verb action that handles no specific class. But we can’t declare it exactly the same way we declare the other noun-as-verb actions (because it would be handled the same way, so we’d still have the same problem). Instead we set an arbitrary “badness” flag on the results that’s not “bad” enough to throw an exception. This will make the parser silently drop us on the floor and continue processing, which is exactly what we want. In this case we use noteBadPrep(), which is normally used for ambiguous prepositional phrases:

DefineTAction(Kludge);
VerbRule(Kludge) singleDobj: KludgeAction verbPhrase = 'kludge/kludging (what)'
        resolveNouns(srcActor, dstActor, results) {
                inherited(srcActor, dstActor, results);
                results.noteBadPrep();
        }
;

This creates an additional action that will never be used, but it will catch all of the noun-as-verb statements that aren’t handled by a different noun-as-verb action (that is, one that doesn’t mark the noun phrase as having weak phrasing).

Adding a couple macros to make declaring things easier and rolling it into a module that you can find here, we can get all of the behaviors we want with:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

#include "nounAsVerb.h"

DefineNounAsVerb(Foo, Pebble);
DefineNounAsVerb(Bar, Rock);

class Pebble: Thing
        dobjFor(Foo) { action() { "You foo the pebble. "; } }
;

class Rock: Thing
        dobjFor(Bar) { action() { "You bar the rock. "; } }
;

startRoom: Room 'Void' "This is a featureless void. ";
+me: Person;
+pebble: Pebble 'small round pebble' 'pebble' "A small, round pebble. ";
+rock: Rock 'ordinary rock' 'rock' "An ordinary rock. ";
+stone: Thing 'nondescript stone' 'stone' "A nondescript stone. ";

versionInfo:    GameID;
gameMain:       GameMainDef initialPlayerChar = me;

That is, >PEBBLE is handled as “foo the pebble”, >ROCK is handled as “bar the rock”, and >STONE is handled as “parse this as if none of this noun-as-verb nonsense was here”:

Void
This is a featureless void.

You see a stone, a pebble, and a rock here.

>pebble
You foo the pebble.

>rock
You bar the rock.

>stone
The story doesn't understand that command.

This is a bit of a kludge and, notably, it doesn’t have any disambiguation method for objects that match multiple noun-as-verb actions (in our example, an object that’s an instance of both Pebble and Rock).

But figuring out this much was involved enough that I figured I’d put it out there anyway.

3 Likes

I may have missed your intent, but does this not work?

DefineTAction(Verbless) ...
modify Thing
   dobjFor(Verbless) { verify { illogical('The default response. '); } }
;
modify Pebble
   dobjFor(Verbless) { verify { }
      action { if(ofKind(Stone)) "The case where we're both, but Pebble was last in the superclass list. "; 
          else "The pebble response.  "; } }
;
modify Stone
   dobjFor(Verbless) { verify { }
      action { if(ofKind(Pebble)) "The case where we're both, but Stone was last in the superclass list. ";
         else "The pebble response.  "; } }
;

Or skip the if(ofKind) bits, and just customize action() for hybrid objs. Make a subclass of Pebble, Stone and it has its own dobjFor

1 Like

That’s more or less where I started out (only it was a travel action for moving to adjacent rooms by typing their name instead of a compass direction).

One of the things that a solution of that form doesn’t do is handle multiple actions. You can kinda fake it by just making one actual action behave as any number of “virtual” actions by implementing the different behaviors via conditional spaghetti in the action handlers. But then you’ve got the confusing usage where a single action can mean completely different things depending on context (Verbless might be “move to an adjacent room” in one case or “comment on something the guy you’re in conversation with just said”, to use two examples I’m actually working on).

It also doesn’t fall through to the normal parser behavior. In this case the thing that would end up handling the command is the illogical() in the verify() method in Thing.dobjFor(Verbless). You could fake this as well (by having the verify method output playerMessages.commandNotUnderstood(gActor), for example). But I want processing to actually fall through, because I don’t necessarily know what else might handle the thing if I don’t. So just general coding campground etiquette (leave everything in the condition you found it) when you’re not handling a case you actually want to handle.

Basically the thing you’re talking about is what I started out with, which gets you like 90% of the way there. And then doing the remaining “little” bits ended up being way more complicated that it seems like it ought to be.

1 Like

Lol, well now I have more refactoring ahead! I ‘solved’ that problem with my favorite crutch StringPreParser.

StringPreParser
	doParsing(str, which) {
        local nounVerbPebbleRegex = R'<NoCase>^<Space>*(pebble)<Space>*$';
        local nounVerbRockRegex = R'<NoCase>^<Space>*(rock)<Space>*$';

        if (rexMatch(nounVerbPebbleRegex, str))
             str = 'foo ' + rexGroup(1)[3];
        else if (rexMatch(nounVerbRockRegex, str))
             str = 'bar ' + rexGroup(1)[3];
		return str;
     }
;

Where foo and bar are traditionally defined Verbs.

Obviously, this has the downside of needing a full repeat of the corresponding vocabWords’ nouns in the Regex. While I could programmatically derive it from the object’s vocabWords string, that would still not accommodate dynamically added vocabulary. Next step would be to develop the object’s raw noun list. In my case, the application is fairly surgical, and the objects in question are one-word noun ones, so didn’t bother.

IAC, I know I have a PreParserInit problem, and this will be another step on my road to recovery.

2 Likes

Yeah, that’s another road that I walked down for awhile—using a PreinitObject to iterate over all objects, creating a hash table of their vocabulary, with the leaf nodes containing a callback method defined on the individual object. In addition to being a headache to get working with runtime modified vocabulary, it also (in my case) creates a bunch of disambiguation problems if you’ve got any objects with identical vocabulary.

Some light thread necromancy here.

I’ve noticed a problem with the noun-as-verb logic I’ve been using. To summarize:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

startRoom: Room 'Void' "This is a featureless void. ";
+me: Person;
+pebble: Pebble 'small round pebble' 'pebble' "A small, round pebble. ";
+rock: Rock 'ordinary rock' 'rock' "An ordinary rock. ";
+stone: Thing 'nondescript stone' 'stone' "A nondescript stone. ";

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

#define DefineNounAsVerb(name, cls) \
        DefineTActionSub(name, NounAsVerb); \
        VerbRule(name) singleDobj: name##Action \
        verbPhrase = 'fake/faking (what)' \
        nounAsVerbClass = cls

class NounAsVerb: TAction
        nounAsVerbClass = nil

        resolveNounsAsVerbs(srcActor, dstActor, results) {
                local r;

                if((dobjList_ == nil) || (nounAsVerbClass == nil))
                        return;

                r = nil;
                dobjList_.forEach(function(o) {
                        if(o.obj_ && o.obj_.ofKind(nounAsVerbClass)) {
                                r = true;
                        }
                });

                if(r != true) {
                        results.noteWeakPhrasing(100);
                }
        }

        resolveNouns(srcActor, dstActor, results) {
                inherited(srcActor, dstActor, results);
                resolveNounsAsVerbs(srcActor, dstActor, results);
        }
;

DefineTAction(NounAsVerbCatchAll);
VerbRule(NounAsVerbCatchAll)
        singleDobj: NounAsVerbCatchAllAction
        verbPhrase = 'catch/catching (what)'

        resolveNouns(srcActor, dstActor, results) {
                inherited(srcActor, dstActor, results);
                results.noteBadPrep();
        }
;

modify Thing
        dobjFor(Foo) { verify() { illogical('You can\'t foo that.'); } }
        dobjFor(Bar) { verify() { illogical('You can\'t bar that.'); } }
;

class Pebble: Thing
        dobjFor(Foo) {
                verify() { nonObvious; }
                action() { "You foo the <<name>>. "; }
        }
;
class Rock: Thing
        dobjFor(Bar) {
                verify() { nonObvious; }
                action() { "You bar the <<name>>. "; }
        }
;
DefineNounAsVerb(Foo, Pebble);
DefineNounAsVerb(Bar, Rock);

This gives us a pebble, a rock, and a stone. Typing >PEBBLE uses the action Foo on the pebble, >ROCK does Bar on the rock, and >STONE falls through and is handled as if none of the noun-as-verb logic was here.

Void
This is a featureless void.

You see a stone, a pebble, and a rock here.

>pebble
You foo the pebble.

>rock
You bar the rock.

>stone
The story doesn't understand that command.

So far so good. The problem is that poorly formed verb phrases now get matched by the noun-as-verb actions and are now treated as if the verb phrase is a noun:

>ask
You see no ask here.

>ask about pebble
You see no ask about pebble here.

Since the Action that’s doing the noun-as-verb work matches a bare singleDobj, I assume this means that the action’s resolveNouns() needs to throw an exception or something if there are no matches or something like that.

Does anyone else know enough about the parser internals (or can suggest some documentation on them) to throw some light onto this?

1 Like

This is merely a guess… have you tried putting [badness 100] after the VerbRule(NounAsVerbCatchAll)?

Doesn’t help.

You can in fact comment out the entire catch-all definition and get the same behavior as far as “capturing” verb phrases goes. The only thing it (the catch-all) does is handle the “fall through” when you have a noun phrase that doesn’t match any of the defined noun-as-verb actions.

So in this case commenting out the NounAsVerbCatchAll definition just means that >STONE will now output “You can’t bar that.” instead of “The story doesn’t understand that command.”

1 Like

And for clarity: the reason the above is true is because of the entire thing uses a very kludgy mechanism under the hood: NounAsVerb checks to see if anything in the action’s dobjList_ is an instance of the class it uses to figure out if it’s supposed to handle the object (so Foo checks to see if each element of dobjList_ is an instance of Pebble, for example). If none are, then resolveNoun() calls the response object’s noteWeakPhrasing() method.

Independently, NounAsVerbCatchAllAction.resolveNouns() always calls results.noteBadPrep().

The gimmick being that noteBadPrep() is “better” than noteWeakPhrasing(), and so the catch-all will be preferred to a singleDobj phrase that doesn’t get matched in some NounAsVerbAction instance’s resolveNouns().

1 Like

It looks like the problem, or one of them anyway, is that the existence of the noun-as-verb productions prevents the parser from even considering a number of actions that would have otherwise been considered.

My first thought was to change the noun-as-verb action’s resolveNouns() to flag the action if the action doesn’t want to handle anything in the dobjList_, and then write a slightly tweaked executeCommand() loop that looks for the flag on the actions in the matchlist. And then just skip all the actions with the flag (instead of just accepting the first rank-sorted match, which is what it does by default).

But the presence of the noun-as-verb grammatical productions doesn’t just add things to the matchlist (namely the new actions)…it removes actions that would otherwise be there.

Gonna need to do some more sourcediving.

So whatever’s happening, it seems to be happening in parseTokens(). Which doesn’t appear to be documented, and is an intrinsic that can’t be modified (without recompiling the interpreter).

Sooooo…looks like the entire noun-as-verb approach I use above in the thread is unfixably broken.

Funny… I just spewed [badness 100] as a guess. It turns out that ask already has a badness of 500 in the library. So put your badness at 600 and ask will not use your NounAsVerb behavior.

Well, since we’re kind of hacking things up here already, here’s a little more hackiness that seems to work (oh, I’m sure if you extend the application of it, you’ll find ways that it comes short. But it at least gets you the transcript you were going for in the post!)

The breakdown: give NounAsVerb and the catchall both a badness of 600. Give NounAsVerb a noVocabMatch if it can’t find a dobj in scope (as this is the worst fault amongst CommandRankings), and just to be safe, give the catchall a noMatch.
Then modify the noMatch method of both of those actions, to basically disguise that we got recognized as a command. (Instead of printing a dqstring, you could make a direct call to libMessages.commandNotUnderstood.) Also note that we have to tweak the filterAmbig methods, because otherwise if you type ‘window’ in a room with two windows, it will ask for which one, instead of immediately saying ‘story doesn’t understand’.

#define DefineNounAsVerb(name, cls) \
        DefineTActionSub(name, NounAsVerb) ;\
        VerbRule(name) [badness 600] singleDobj: name##Action \
        verbPhrase = 'fake/faking (what)' \
        nounAsVerbClass = cls

class NounAsVerb: TAction
	noMatch(ac,a,t) { "The story doesn't understand that command. "; }
	filterAmbiguousDobj(lst, requiredNum, np) {
           return lst.length? [lst[1]] : lst ; }

        nounAsVerbClass = nil

        resolveNounsAsVerbs(srcActor, dstActor, results) {
                local r;
                if((dobjList_ == nil) || (nounAsVerbClass == nil))
                        return;
                r = nil;
                dobjList_.forEach(function(o) {
                        if(o.obj_ && o.obj_.ofKind(nounAsVerbClass)) {
                                r = true;
                        }
                });
                if(r != true) { 
                        results.noVocabMatch(self, '');
                }
        }

        resolveNouns(srcActor, dstActor, results) {
                inherited(srcActor, dstActor, results);
                resolveNounsAsVerbs(srcActor, dstActor, results) ;
        }
;
DefineTAction(NounAsVerbCatchAll)
	noMatch(ac,a,t) { "The story doesn't understand that command. "; }
	filterAmbiguousDobj(lst, requiredNum, np) {
        return lst.length? [lst[1]] : lst ; };
VerbRule(NounAsVerbCatchAll) [badness 600]
        singleDobj: NounAsVerbCatchAllAction
        resolveNouns(srcActor, dstActor, results) {
                inherited(srcActor, dstActor, results);
                results.noMatch(self,'');
        }
;

With this code, I got:

You see a stone, a pebble, a rock, an east window, and a west window here. 


>stone
The story doesn't understand that command. 


>pebble
You foo the pebble. 


>rock
You bar the rock. 


>window
The story doesn't understand that command. 


>winddow
The word “winddow” is not necessary in this story. 


(If this was an accidental misspelling, you can correct it by typing OOPS followed by the corrected word now. Any time the story points out an unknown word, you can correct a misspelling using OOPS as your next command.)


>ask
What do you want to ask about? 


>put
What do you want to put? 


>
1 Like

This is good. I tried a few things similar to this, only instead of faking command responses via noMatch() I was catching things in a slightly re-written executeCommand() loop (throwing a custom exception and then catching it).

I think what I want to do now is actually re-write the whole thing to mostly just use a modified executeCommand(), and have DefineNounAsVerb register objects and/or keywords to be matched as if they’re verbs, and handle that stuff inside the parseTokenLoop: loop in executeCommand().

Fiddling around with executeCommand() is a little more heavyweight (in terms of touching “this may void the warranty” bits), but it eliminates the need to fake out normal grammatical processing.

1 Like

I’ll be interested to see your code if you get it working the same via executeCommand…

I haven’t lifted a finger to pursue this as of yet, but would it be possible to simply create a new class of Prod to handle this, whose grammar only matches objects of the class you’re interested in?
Something like:

VerbRule(Foo) singlePebbleDobj : FooAction

etc., where singlePebbleDobj is the Prod you’d create, maybe tweaking the getVocabMatchList method in similar ways to the Tech Manual…

1 Like

Prod ?

You mean… ?

Yeah, that’s actually one of the first approaches I tried. I didn’t spend as much time with it as with the approach presented earlier in the thread, mostly because it’s slightly more heavyweight (you have to create all the new dobj productions in addition to the new actions), and you still have many of the same problems we’ve recently discussed here (that is, falling through cleanly when you’ve got a command line without an “real” verbs on it). It’s possible that you could address the latter with aggressive use of [badness], like you did above.

I also remember running into scope problems, but I wasn’t working on this particular approach long enough (and this was a couple months ago) so I don’t remember the specifics. But stuff like using >PEBBLE when there are no pebbles in scope producing “What do you want to pebble?” (or whatever’s defined in the VerbPhrase) instead of giving you the standard noun-not-in-scope failure message.

Anyway, as far as doing something with executeCommand(), I’ll either ping the thread when I update the repo (or possibly deprecate the current module and replace it with a new module called something like keywordAction…in addition to getting the noun-as-verb stuff working, I want to integrate some dialog/conversation stuff I’ve been working on, which uses context-specific keywords, and “directionless” travel).

“Production”. Basically a semantic rule for substituting a particular arrangement of lexical tokens with something else.

And I might implement a executeCommand() “replacement” without actually having to modify the existing executeCommand(). I think the only place that executeCommand() gets called is from PendingCommandToks.executePending(), so something like:

modify PendingCommandToks
        executePending(targetActor) {
                if(checkKeywordActions(targetActor, issuer_, tokens_,
                        startOfSentence_))
                        return;
                executeCommand(targetActor, issuer_, tokens_, startOfSentence_);
        }
;

…will work, where checkKeywordActions() is what does all of the “stuff”.

That would require replicating some of the bookkeeping stuff (like setting up the sense context) that’s done inside executeCommand(), so I’ll have to play with it more get a feel for whether it makes more sense to replace executeCommand() outright or to stick all the changes into a sort of parallel loop that’ll only get used for stuff like the noun-as-verb stuff.

I’m also not sure how expensive context switching in T3. Because another approach would be to break out the special cases into their own functions (instead of having a single, all-in-one executeCommand() function).

1 Like