The Problem of Inference in Inform

Branching from the discussion at Pouring it into, Does the player mean - #21 by otistdog

OK, calling all Mad Scientists: Let’s fix the inference problem.

Based on my experiments, this is both easier and harder than it sounds. Certain relevant parsing features are not exposed at the I7 level and not easy to represent, but that can be worked around. The bigger problem is trying to impose an intuitive model of the inference process over what’s actually going on, so as to make something easy for authors to use.

@Draconis: You have pretty much written off inference as a nuisance best removed. What would it take to make the subsystem useful to you?

@Zed: You’ve had a long-standing interest in “sanity check rules.” What form would those take, and can they be made applicable here?

Some approaches I’ve tried:

  • Fine-grained inference scoring control

    • author can write arbitrary rules to assign inference score bonuses and penalties
    • pros: complete control over the score used to infer objects
    • cons: hard to anticipate how combined scores will stack up for a given object
  • Fine-grained control of existing model

    • author can write rules to decide whether or not scoring components are applied, but not dictate scores
    • pros: fairly easy to eliminate “interference” of automatic rules in particular situations
    • cons: have to understand underlying model in detail to know what rules to write
  • Double-duty for DTPM rules

    • the inference process consults DTPM rules when deciding inference scoring
    • pros: seems closer to the intuitive model most people have of what DTPM rules should do
    • cons: makes understanding the full impact of DTPM rules even harder

Well…

Overall, I think the current inference system is a solution looking for a problem, and creating new problems in the process. The only times I’ve seen someone type TAKE without a noun, it’s because they were exploiting inference to figure out if there’s a single object in a dark room, or something along those lines. (This was a frequent “feature” of the Phoenix games.) If someone types UNSCREW PANEL without a second noun, it’s either because they didn’t expect it to require a second noun at all, or the choice of second noun is so obvious they thought it could be implied.

In either case, “(with the apple)” is not an appropriate response. If there’s an obvious choice, that has to be decided based on the specific action and the specific circumstances, not on the number of things in scope. If there isn’t an obvious choice (even if there’s only one thing in scope), “you must supply a noun” or “with what?” is far more appropriate.

The normal way to handle this in I7 doesn’t require an inference system at all. If an action has an obvious second noun, like UNSCREW needing a screwdriver or FILL needing a liquid, then the author is the best one to determine this; they can make a separate one-noun action (like Locksmith does for UNLOCK), which either fills in an appropriate second noun (if possible), or prints a suitable error message (if not). You can even do this without another action using “supplying a missing [second] noun”, if you want.

For any other case, an error message is better than grabbing the nearest item. Imo, this would have all the benefits of the current inference system, and also avoid GET grabbing an undescribed privately-named scenery thing that you’ve tried very hard to keep out of scope, or UNLOCK DOOR supplying “(with the door)”.

The main thing I7 can’t currently do, which I think would be helpful here, is let an author print “with what?” and have the next command fill in the missing [second] noun. Currently, that can only be done by the parser internals in a way that’s generally inaccessible to authors. If Inform could do that, then authors could handle these things entirely at the I7 level, and inference would be needed no more. Dialog handles this with a global variable holding the incomplete action (with nil in place of the missing noun or second noun). When the parser sees a noun-only command, it checks that variable and returns the completed action if possible. Either way, the variable is cleared after the next input.

(Does Inform have a name for this “with what?” mechanism? Dialog calls them “implicit actions”, but that means something entirely different in I7. ZIL calls them “orphan commands”, but that technically applies to any ambiguous command, like PUSH BUTTON with two buttons present. For lack of a better term, I might call them incomplete actions.)

Agreed.

The place in NounDomain where they’re asked is after the .Incomplete label. But given that we’re talking about something during parsing, I think I’d be happier with incomplete command to avoid implying an action exists yet. (To my knowledge, there’s no hint of an existing name.)

It’s probably wholly infeasible to try to crowbar this into the current code, but conceptually it seems like it’d be something close to ideal if this were when supplying a missing noun and supplying a missing second noun occurred instead of requiring us to make separate grammar lines.

I need to be off to bed, but I’ll think more about it later…

OK – the opening bid is “Please just make it stop.”

Here’s a typical example of out-of-the-box interaction:

Lab
A typical Mad Scientist's laboratory.

You can see a beaker washer and a scarred lab table (on which are a beaker of red liquid, a beaker of green liquid and a beaker of blue liquid) here.

>I
You are carrying:
  a clipboard

>PUT
(on the clipboard)
You can't wear that!

>PUT CLIPBOARD
(in the clipboard)
You can't put something inside itself.

It’s not that hard to prevent inference (i.e. trying to determine a noun without corresponding command words), yielding:

Lab
A typical Mad Scientist's laboratory.

You can see a beaker washer and a scarred lab table (on which are a beaker of red liquid, a beaker of green liquid and a beaker of blue liquid) here.

>I
You are carrying:
  a clipboard

>PUT
What do you want to put on?

>PUT CLIPBOARD
What do you want to put the clipboard in?

>TABLE
That can't contain things.

>PUT CLIPBOARD ON
You can't wear that!

>PUT CLIPBOARD ONTO
What do you want to put the clipboard onto?

I’m not sure it’s a huge improvement, though it does have the benefit of not actually going ahead with the wrong actions (mostly).

Arguably part of the problem here is the set of default grammar lines around >PUT. There’s no ability to directly control precedence of those in I7.

The case of >PUT CLIPBOARD ON is interesting. Assuming that it’s not a wearable object, a person would probably judge this far more likely to be an incomplete command lacking a second noun than a complete but nonsensical command.

Actually, it can. It just prefers to infer.

No, it’s definitely possible. The drawback is that it requires a mini-library of DTPM rules that cover the built-in grammar lines.

I have no solutions to offer, but as a player, this certainly isn’t desirable:

"Untitled"

Laboratory is a room.
>give
(yourself yourself)
(first taking yourself)
You are always self-possessed.

Now, this is an edge case, but it still highlights that there’s something wrong.

Reviewing the guts, the inference process does consult DTPM rules out-of-the-box, as part of ScoreMatchL(), it’s just that the details of constructing correct ones for two-parameter actions often are not obvious. So maybe part of the problem is the paucity of DTPM rules in the Standard Rules. However, there’s no out-of-the-box to support a “never infer this” decision (something offered by Disambiguation Control, I think).

Zed’s right that the case of a “short grammar” for an action bypasses inference and drops the problem of determining nouns into the for supplying a missing... activities, and that some default rules for those rulebooks that consult DTPM could help. The main drawback of working with those activities is that they are post-parsing and therefore can’t take advantage of the asking which do you mean activity or the prompt to provide more input.

Just what draconis meant by

let an author print “with what?” and have the next command fill in the missing [second] noun.

There’s a clearly a use case for being able to say “try getting (something the player will tell us what it is)” at action-processing time.

Having any version of that would be a great improvement imo, but for bonus points: Dialog also lets the author specify what sort of missing noun the parser should look for. That way, if you PUSH something that’s pushable between rooms, Dialog will specifically look for a direction in the next input, and turn it into PUSH TROLLEY EAST or whatever. If you UNSCREW PANEL, it’s looking for a thing, so typing EAST will produce “going east” rather than “unscrewing the panel with east”.

(Dialog specifically only allows “thing” and “direction”, which is enough for 90% of cases, but something like TURN DIAL asking “to which number?” requires modifying the library.)

Oh, OK – Thanks, zarf. I misunderstood since I was equating “next command” with “next input.”

As Zed pointed out, the closest equivalent is what’s seen at .Incomplete in NounDomain(). That part of the code asks for more information mid-parsing of the current command, but it is generally never executed because it is treated as a last resort for when inference fails.

Unless you specifically want there to be separate action-processing cycles (something that impacts turn sequence processing), then the interaction pattern of the player being prompted for more to complete a command is quite similar to Dialog’s player-facing behavior. That’s what’s going on in the example transcript above – the modification just prevents inference entirely, so it falls back to the .Incomplete block.

@Zed, I think I was reading your post too quickly, too. Are you saying that inference should consult SMN and SMSN, so that a Standard Rule like:

Rule for supplying a missing noun while an actor smelling (this is the ambient odour rule):
	now the noun is the touchability ceiling of the player.

would be invoked to set the noun while inferring for the grammar line

Understand "smell [something]" as smelling.

instead of relying on the existence of the “short grammar” from

Understand "smell" as smelling.

and making the noun assignment post-parsing?

If you disable the short grammar for the smelling action, you can get the same basic result from:

After deciding the scope of the player while smelling:
	place the location in scope.

Does the player mean smelling the location:
	it is likely.

Rule for clarifying the parser's choice of a room while smelling:
	do nothing.

I think it should be possible to hotwire those activities into the main parsing sequence, though.

Not sure how closely this is related to the inference problem, but… blocking phrases that ask for input and then return (like player consents, but dealing with non-boolean types) would be helpful in a wide variety of cases, possibly including this one (perhaps you could request a pushable thing from the player in for supplying a missing noun, for example).

This is not a good use for a blocking input phrase. Any disambiguation question should allow for the possibility that the player will ignore it and type a whole new command. Also you might need multiple stages of questions. (“With what?” KNIFE “Which do you mean, the rusty knife or the nasty knife?” NASTY)

The feature really has to be tightly integrated with the parser loop.

A prototype of using SMN/SMSN to override inference is pretty straightforward. There might be some edge case bugs, but as a proof-of-concept, it works.

With some test rules:

For supplying a missing noun while smelling (this is the smell the environment if no noun supplied rule):
	now the noun is the touchability ceiling of the current actor.

For clarifying the parser's choice of a room while smelling:
	do nothing.

For supplying a missing second noun while inserting into (this is the choose a random container if no second noun supplied rule):
	let vessel be a random ((touchable container that is not the noun) that is not enclosed by the noun) that does not enclose the noun;
	unless vessel is nothing:
		now second noun is vessel.

For supplying a missing second noun while putting on (this is the choose a random supporter if no second noun supplied rule):
	let platform be a random ((touchable supporter that is not the noun) that is not enclosed by the noun) that does not enclose the noun;
	unless platform is nothing:
		now second noun is platform.

the scenario yields interaction such as:

Lab
A typical Mad Scientist's laboratory.

You can see a beaker washer and a scarred lab table (on which are a 100mL beaker, a 250mL beaker and a 500mL beaker) here.

>SHOWVERB SMELL
Verb 'smell' 'sniff' 
	 * noun -> Smell

>ACTIONS
Actions listing on.

>SMELL
[smelling Lab]
You smell nothing unexpected.
[smelling Lab - succeeded]

>ACTIONS OFF
Actions listing off.

>SHOWVERB PUT
Verb 'put' 
	 * 'on' noun=Routine(344354) -> Wear
	 * noun=Routine(344404) 'on' -> Wear
	 * 'down' multiheld -> Drop
	 * multiheld 'down' -> Drop
	 * multiexcept 'in' / 'inside' / 'into' noun -> Insert
	 * multiexcept 'on' / 'onto' noun -> PutOn

>PUT CLIPBOARD
(in the 250mL beaker)
You put the clipboard into the 250mL beaker.

>PUT CLIPBOARD ON
(the scarred lab table)
(first taking the clipboard)
You put the clipboard on the scarred lab table.

>PUT 100ML
(in the 250mL beaker)
(first taking the 100mL beaker)
You put the 100mL beaker into the 250mL beaker.

>PUT 250ML
(in the 500mL beaker)
(first taking the 250mL beaker)
You put the 250mL beaker into the 500mL beaker.

>PUT 500ML
What do you want to put the 500mL beaker in?

>TAKE
What do you want to take?

>500ML
Taken.

>I
You are carrying:
  a 500mL beaker
	a 250mL beaker
	  a 100mL beaker

Note that I redefined the grammar for “put” to require a [wearable thing] so that the parser would stop wanting to infer the wearing action all the time.

Also note that it’s happenstance that the smaller beakers ended up inside larger ones, since that’s not being enforced… which brings up a general point: If you want some intelligent inference to happen, you have to provide the logic for it somewhere, which means becoming familiar with the rules already in place and how activities are processed, so that you can craft special case rules.

EDIT: Also also note, the beaker washer is just a thing, not a container or supporter, which is why it’s ignored when inserting it into or putting it on.

Came back to this thread and found a never-finished draft I’d left unposted.

My suspicion of difficulty was in regard to the notion of recontextualizing the supplying a missing noun activities, per se, 'cause those could potentially set the a noun to an object that hadn’t been in scope. (And if we were handling missing nouns at this stage, there’d be no remaining point to those activities occurring during generate action.)

Like I said above, it’d be ideal if we didn’t need to manually specify missing-noun variants of grammar lines, but for the parser to automatically prompt for a noun. Then we could change that grammar line to:

Understand "put [wearable thing] on" as wearing.

and a PUT <non-wearable-thing> ON command would get something like

What do you want to put it on?


New stuff, now.

Sort of. My thinking is functionality like that provided by those activities is what should happen instead of inference as it exists today. But I didn’t mean that the parser should literally invoke the existing missing noun activities while the tests to potentially invoke those same activities during ActionPrimitive live on. (I’m not sure whether there’d still be a point to having such a facility during action processing if the parser had this, but maybe there’s a compelling reason I’m not thinking of.)

Care to share the prototype that enables this? I took a whack at it and got it to work almost like yours, but it refuses to consider the scarred lab table as second noun with >PUT CLIPBOARD ON because the lab table does enclose the clipboard at that point so it prompts

What do you want to put the clipboard on?

instead.

Wouldn’t the best way to deal with inference be to do something like this?

"Test" by Climbingstars

Include (-

[ Parser__parse
	syntax line num_lines line_address i j k token l m inferred_go;
	cobj_flag = 0;
	parser_results-->ACTION_PRES = 0;
	parser_results-->NO_INPS_PRES = 0;
	parser_results-->INP1_PRES = 0;
	parser_results-->INP2_PRES = 0;
	meta = false;

	if (held_back_mode) {
		held_back_mode = false; wn = hb_wn;
		if (verb_wordnum > 0) i = WordAddress(verb_wordnum); else i = WordAddress(1);
		j = WordAddress(wn);
		if (i<=j) for (: i<j : i++) i->0 = ' ';
		i = NextWord();
		if (i == AGAIN1__WD or AGAIN2__WD or AGAIN3__WD) {
			! Delete the words "then again" from the again buffer,
			! in which we have just realised that it must occur:
			! prevents an infinite loop on "i. again"

			i = WordAddress(wn-2)-buffer;
			if (wn > num_words) j = INPUT_BUFFER_LEN-1;
			else j = WordAddress(wn)-buffer;
			for (: i<j : i++) buffer3->i = ' ';
		}

		VM_Tokenise(buffer, parse);
		jump ReParse;
	}

	.ReType;

	cobj_flag = 0;
	actors_location = ScopeCeiling(player);
	BeginActivity(READING_A_COMMAND_ACT); if (ForActivity(READING_A_COMMAND_ACT)==false) {
		Keyboard(buffer,parse);
		num_words = WordCount(); players_command = 100 + num_words;
	} if (EndActivity(READING_A_COMMAND_ACT)) jump ReType;

	.ReParse;

	parser_inflection = name;

	! Initially assume the command is aimed at the player, and the verb
	! is the first word

	num_words = WordCount(); players_command = 100 + num_words;
	wn = 1; inferred_go = false;

	LanguageToInformese();
	! Re-tokenise:
	VM_Tokenise(buffer,parse);

	num_words = WordCount(); players_command = 100 + num_words; token_filter = 0;
	allow_plurals = true; ResetDescriptors();

	k=0;
	#Ifdef DEBUG;
	if (parser_trace >= 2) {
		print "[ ";
		for (i=0 : i<num_words : i++) {

			#Ifdef TARGET_ZCODE;
			j = parse-->(i*2 + 1);
			#Ifnot; ! TARGET_GLULX
			j = parse-->(i*3 + 1);
			#Endif; ! TARGET_
			k = WordAddress(i+1);
			l = WordLength(i+1);
			print "~"; for (m=0 : m<l : m++) print (char) k->m; print "~ ";

			if (j == 0) print "?";
			else {
				#Ifdef TARGET_ZCODE;
				if (UnsignedCompare(j, HDR_DICTIONARY-->0) >= 0 &&
					UnsignedCompare(j, HDR_HIGHMEMORY-->0) < 0)
					print (address) j;
				else print j;
				#Ifnot; ! TARGET_GLULX
				if (j->0 == $60) print (address) j;
				else print j;
				#Endif; ! TARGET_
			}
			if (i ~= num_words-1) print " / ";
		}
		print " ]^";
	}
	#Endif; ! DEBUG
	verb_wordnum = 1;
	actor = player;
	actors_location = ScopeCeiling(player);
	usual_grammar_after = 0;

	.AlmostReParse;

	scope_token = 0;
	action_to_be = NULL;

	! Begin from what we currently think is the verb word

	.BeginCommand;
	wn = verb_wordnum;
	verb_word = NextWordStopped();
	! If there's no input here, we must have something like "person,".

	if (verb_word == -1) {
		best_etype = STUCK_PE; jump GiveError;
	}
	if (verb_word == comma_word) {
		best_etype = COMMABEGIN_PE; jump GiveError;
	}

	! Now try for "again" or "g", which are special cases: don't allow "again" if nothing
	! has previously been typed; simply copy the previous text across

	if (verb_word == AGAIN2__WD or AGAIN3__WD) verb_word = AGAIN1__WD;
	if (verb_word == AGAIN1__WD) {
		if (actor ~= player) {
			best_etype = ANIMAAGAIN_PE;
			jump GiveError;
		}
		#Ifdef TARGET_ZCODE;
		if (buffer3->1 == 0) {
			PARSER_COMMAND_INTERNAL_RM('D'); new_line;
			jump ReType;
		}
		#Ifnot; ! TARGET_GLULX
		if (buffer3-->0 == 0) {
			PARSER_COMMAND_INTERNAL_RM('D'); new_line;
			jump ReType;
		}
		#Endif; ! TARGET_
		for (i=0 : i<INPUT_BUFFER_LEN : i++) buffer->i = buffer3->i;
		VM_Tokenise(buffer,parse);
		num_words = WordCount(); players_command = 100 + num_words;
		jump ReParse;
	}

	! Save the present input in case of an "again" next time

	if (verb_word ~= AGAIN1__WD)
		for (i=0 : i<INPUT_BUFFER_LEN : i++) buffer3->i = buffer->i;

	if (usual_grammar_after == 0) {
		j = verb_wordnum;
		i = 0; ! force parsing without grammar property; see DM4 p. 140
		#Ifdef DEBUG;
		if (parser_trace >= 2 && i ~= 0 or NULL)
			print " [Grammar property returned ", i, "]^";
		#Endif; ! DEBUG

		if ((i ~= 0 or 1) && (VM_InvalidDictionaryAddress(i))) {
			usual_grammar_after = verb_wordnum; i=-i;
		}

		if (i == 1) {
			parser_results-->ACTION_PRES = action;
			parser_results-->NO_INPS_PRES = 0;
			parser_results-->INP1_PRES = noun;
			parser_results-->INP2_PRES = second;
			if (noun) parser_results-->NO_INPS_PRES = 1;
			if (second) parser_results-->NO_INPS_PRES = 2;
			rtrue;
		}
		if (i ~= 0) { verb_word = i; wn--; verb_wordnum--; }
		else { wn = verb_wordnum; verb_word = NextWord(); }
	}
	else usual_grammar_after = 0;

	if (verb_word == 0) {
		i = wn; verb_word = LanguageIsVerb(buffer, parse, verb_wordnum);
		wn = i;
	}

	! If the first word is not listed as a verb, it must be a direction
	! or the name of someone to talk to

	if (verb_word == 0 || ((verb_word->#dict_par1) & 1) == 0) {
		! So is the first word an object contained in the special object "compass"
		! (i.e., a direction)?  This needs use of NounDomain, a routine which
		! does the object matching, returning the object number, or 0 if none found,
		! or REPARSE_CODE if it has restructured the parse table so the whole parse
		! must be begun again...

		wn = verb_wordnum; indef_mode = false; token_filter = 0; parameters = 0;
		@push actor; @push action; @push action_to_be;
		actor = player; meta = false; action = ##Go; action_to_be = ##Go;
		l = NounDomain(Compass, 0, 0);
		@pull action_to_be; @pull action; @pull actor;
		if (l == REPARSE_CODE) jump ReParse;

		! If it is a direction, send back the results:
		! action=GoSub, no of arguments=1, argument 1=the direction.

		if ((l~=0) && (l ofclass K3_direction)) {
			parser_results-->ACTION_PRES = ##Go;
			parser_results-->NO_INPS_PRES = 1;
			parser_results-->INP1_PRES = l;
			inferred_go = true;
			jump LookForMore;
		}

	} ! end of first-word-not-a-verb

	! Only check for a comma (a "someone, do something" command) if we are
	! not already in the middle of one.  (This simplification stops us from
	! worrying about "robot, wizard, you are an idiot", telling the robot to
	! tell the wizard that she is an idiot.)

	if (actor == player) {
		for (j=2 : j<=num_words : j++) {
			i=NextWord();
			if (i == comma_word) jump Conversation;
		}
	}
	jump NotConversation;

	! NextWord nudges the word number wn on by one each time, so we've now
	! advanced past a comma.  (A comma is a word all on its own in the table.)

	.Conversation;
	j = wn - 1;

	! Use NounDomain (in the context of "animate creature") to see if the
	! words make sense as the name of someone held or nearby

	wn = 1; lookahead = HELD_TOKEN;
	scope_reason = TALKING_REASON;
	l = NounDomain(player, actors_location, CREATURE_TOKEN);
	scope_reason = PARSING_REASON;
	if (l == REPARSE_CODE) jump ReParse;
	if (l == 0) {
		if (verb_word && ((verb_word->#dict_par1) & 1)) jump NotConversation;
		best_etype = MISSINGPERSON_PE; jump GiveError;
	}

	.Conversation2;

	! The object addressed must at least be "talkable" if not actually "animate"
	! (the distinction allows, for instance, a microphone to be spoken to,
	! without the parser thinking that the microphone is human).

	if (l hasnt animate && l hasnt talkable) {
		best_etype = ANIMALISTEN_PE; noun = l; jump GiveError;
	}

	! Check that there aren't any mystery words between the end of the person's
	! name and the comma (eg, throw out "dwarf sdfgsdgs, go north").

	if (wn ~= j) {
		if (verb_word && ((verb_word->#dict_par1) & 1)) jump NotConversation;
		best_etype = TOTALK_PE; jump GiveError;
	}

	! The player has now successfully named someone.  Adjust "him", "her", "it":

	PronounNotice(l);

	! Set the global variable "actor", adjust the number of the first word,
	! and begin parsing again from there.

	verb_wordnum = j + 1;

	! Stop things like "me, again":

	if (l == player) {
		wn = verb_wordnum;
		if (NextWordStopped() == AGAIN1__WD or AGAIN2__WD or AGAIN3__WD) {
			best_etype = ANIMAAGAIN_PE;
			jump GiveError;
		}
	}

	actor = l;
	actors_location = ScopeCeiling(l);
	#Ifdef DEBUG;
	if (parser_trace >= 1)
		print "[Actor is ", (the) actor, " in ", (name) actors_location, "]^";
	#Endif; ! DEBUG
	jump BeginCommand;

	.NotConversation;
	if (verb_word == 0 || ((verb_word->#dict_par1) & 1) == 0) {
		verb_word = UnknownVerb(verb_word);
		if (verb_word ~= 0) jump VerbAccepted;
		best_etype = VERB_PE;
		jump GiveError;
	}
	.VerbAccepted;

	! We now definitely have a verb, not a direction, whether we got here by the
	! "take ..." or "person, take ..." method.  Get the meta flag for this verb:

	meta = ((verb_word->#dict_par1) & 2)/2;

	! You can't order other people to "full score" for you, and so on...

	if (meta == 1 && actor ~= player) {
		best_etype = VERB_PE;
		meta = 0;
		jump GiveError;
	}

	! Now let i be the corresponding verb number...

	i = DictionaryWordToVerbNum(verb_word);

	! ...then look up the i-th entry in the verb table, whose address is at word
	! 7 in the Z-machine (in the header), so as to get the address of the syntax
	! table for the given verb...

	syntax = VM_CommandTableAddress(i);

	! ...and then see how many lines (ie, different patterns corresponding to the
	! same verb) are stored in the parse table...

	num_lines = (syntax->0);

	! ...and now go through them all, one by one.
	! To prevent pronoun_word 0 being misunderstood,

	pronoun_word = NULL; pronoun_obj = NULL;

	#Ifdef DEBUG;
	if (parser_trace >= 1)
		print "[Parsing for the verb '", (address) verb_word, "' (", num_lines, " lines)]^";
	#Endif; ! DEBUG

	best_etype = STUCK_PE; nextbest_etype = STUCK_PE;
	multiflag = false;

	! "best_etype" is the current failure-to-match error - it is by default
	! the least informative one, "don't understand that sentence".
	! "nextbest_etype" remembers the best alternative to having to ask a
	! scope token for an error message (i.e., the best not counting ASKSCOPE_PE).
	! multiflag is used here to prevent inappropriate MULTI_PE errors
	! in addition to its unrelated duties passing information to action routines

	line_address = syntax + 1;

	for (line=0 : line<num_lines : line++) {

		! Unpack the syntax line from Inform format into three arrays; ensure that
		! the sequence of tokens ends in an ENDIT_TOKEN.

		line_address = UnpackGrammarLine(line_address);

		#Ifdef DEBUG;
		if (parser_trace >= 1) {
			if (parser_trace >= 2) new_line;
			print "[line ", line; DebugGrammarLine();
			print "]^";
		}
		#Endif; ! DEBUG

		! We aren't in "not holding" or inferring modes, and haven't entered
		! any parameters on the line yet, or any special numbers; the multiple
		! object is still empty.

		inferfrom = 0;
		parameters = 0;
		nsns = 0; special_word = 0;
		multiple_object-->0 = 0;
		multi_context = 0;
		etype = STUCK_PE; multi_had = 0;

		! Put the word marker back to just after the verb

		wn = verb_wordnum+1;
		advance_warning = -1; indef_mode = false;
		for (i=0,m=false,pcount=0 : line_token-->pcount ~= ENDIT_TOKEN : pcount++) {
			scope_token = 0;

			if (line_ttype-->pcount ~= PREPOSITION_TT) i++;

			if (line_ttype-->pcount == ELEMENTARY_TT) {
				if (line_tdata-->pcount == MULTI_TOKEN) m = true;
				if (line_tdata-->pcount == MULTIEXCEPT_TOKEN or MULTIINSIDE_TOKEN  && i == 1) {
					! First non-preposition is "multiexcept" or
					! "multiinside", so look ahead.

					#Ifdef DEBUG;
					if (parser_trace >= 2) print " [Trying look-ahead]^";
					#Endif; ! DEBUG

					! We need this to be followed by 1 or more prepositions.

					pcount++;
					if (line_ttype-->pcount == PREPOSITION_TT) {
						! skip ahead to a preposition word in the input
						do {
							l = NextWord();
						} until ((wn > num_words) ||
								(l && (l->#dict_par1) & 8 ~= 0));

						if (wn > num_words) {
							#Ifdef DEBUG;
							if (parser_trace >= 2)
								print " [Look-ahead aborted: prepositions missing]^";
							#Endif;
							jump EmptyLine;
						}

						do {
							if (PrepositionChain(l, pcount) ~= -1) {
								! advance past the chain
								if ((line_token-->pcount)->0 & $20 ~= 0) {
									pcount++;
									while ((line_token-->pcount ~= ENDIT_TOKEN) &&
										((line_token-->pcount)->0 & $10 ~= 0))
										pcount++;
								}
								else {
									pcount++;
								}
							}
							else {
								! try to find another preposition word
								do {
									l = NextWord();
								} until ((wn >= num_words) ||
										(l && (l->#dict_par1) & 8 ~= 0));

								if (l && (l->#dict_par1) & 8) continue;

								! lookahead failed
								#Ifdef DEBUG;
								if (parser_trace >= 2)
									print " [Look-ahead aborted: prepositions don't match]^";
								#endif;
								jump LineFailed;
							}
							if (wn <= num_words) l = NextWord();
						} until (line_ttype-->pcount ~= PREPOSITION_TT);

						.EmptyLine;
						! put back the non-preposition we just read
						wn--;

						if ((line_ttype-->pcount == ELEMENTARY_TT) &&
							(line_tdata-->pcount == NOUN_TOKEN)) {
							l = Descriptors();  ! skip past THE etc
							if (l~=0) etype=l;  ! don't allow multiple objects
							k = parser_results-->INP1_PRES; @push k; @push parameters;
							parameters = 1; parser_results-->INP1_PRES = 0;
							l = NounDomain(actors_location, actor, NOUN_TOKEN, true);
							@pull parameters; @pull k; parser_results-->INP1_PRES = k;
							#Ifdef DEBUG;
							if (parser_trace >= 2) {
								print " [Advanced to ~noun~ token: ";
								if (l == REPARSE_CODE) print "re-parse request]^";
								else {
									if (l == 1) print "but multiple found]^";
									if (l == 0) print "error ", etype, "]^";
									if (l >= 2) print (the) l, "]^";
								}
							}
							#Endif; ! DEBUG
							if (l == REPARSE_CODE) jump ReParse;
							if (l >= 2) advance_warning = l;
						}
					}
					break;
				}
			}
		}

		! Slightly different line-parsing rules will apply to "take multi", to
		! prevent "take all" behaving correctly but misleadingly when there's
		! nothing to take.

		take_all_rule = 0;
		if (m && params_wanted == 1 && action_to_be == ##Take)
			take_all_rule = 1;

		! And now start again, properly, forearmed or not as the case may be.
		! As a precaution, we clear all the variables again (they may have been
		! disturbed by the call to NounDomain, which may have called outside
		! code, which may have done anything!).

		inferfrom = 0;
		parameters = 0;
		nsns = 0; special_word = 0;
		multiple_object-->0 = 0;
		etype = STUCK_PE; multi_had = 1;
		wn = verb_wordnum+1;
		m = true;
		for (pcount=1 : : pcount++)
			if (line_token-->(pcount-1) == ENDIT_TOKEN) {
				if (pcount >= 2) {
					while ((((line_token-->(pcount-2))->0) & $10) ~= 0) pcount--;
					if (line_ttype-->(pcount-2) == PREPOSITION_TT) {
						l = -1;
						while (true) {
							m = NextWordStopped();
							if (m == -1) break;
							l = m;
						}
						if (PrepositionChain(l, pcount-2) == -1) {
							m = false;
							#Ifdef DEBUG;
							if (parser_trace >= 2)
								print "[line rejected for not ending with correct preposition]^";
							#Endif; ! DEBUG
						} else m = true;
					}
				}
				break;
			}
		wn = verb_wordnum+1;

		if (m) for (pcount=1 : : pcount++) {
			pattern-->pcount = PATTERN_NULL; scope_token = 0;

			token = line_token-->(pcount-1);
			lookahead = line_token-->pcount;

			#Ifdef DEBUG;
			if (parser_trace >= 2)
				print " [line ", line, " token ", pcount, " word ", wn, " : ", (DebugToken) token,
				"]^";
			#Endif; ! DEBUG

			if (token ~= ENDIT_TOKEN && wn <= num_words) {
				scope_reason = PARSING_REASON;

				l = ParseToken(line_ttype-->(pcount-1), line_tdata-->(pcount-1), pcount-1, token);
				while ((l >= GPR_NOUN) && (l < -1)) l = ParseToken(ELEMENTARY_TT, l + 256);
				scope_reason = PARSING_REASON;

				if (l == GPR_PREPOSITION) {
					if (line_ttype-->(pcount-1)~=PREPOSITION_TT && (line_ttype-->(pcount-1)~=ELEMENTARY_TT ||
						line_tdata-->(pcount-1)~=TOPIC_TOKEN)) params_wanted--;
					l = true;
				}
				else
					if (l < 0) l = false;
					else
						if (l ~= GPR_REPARSE) {
							if (l == GPR_NUMBER) {
								if (nsns == 0) special_number1 = parsed_number;
								else special_number2 = parsed_number;
								nsns++; l = 1;
							}
							if (l == GPR_MULTIPLE) l = 0;
							parser_results-->(parameters+INP1_PRES) = l;
							parameters++;
							pattern-->pcount = l;
							l = true;
						}

				#Ifdef DEBUG;
				if (parser_trace >= 3) {
					print "  [token resulted in ";
					if (l == REPARSE_CODE) print "re-parse request]^";
					if (l == 0) print "failure with error type ", etype, "]^";
					if (l == 1) print "success]^";
				}
				#Endif; ! DEBUG

				if (l == REPARSE_CODE) jump ReParse;
				if (l == false) break;
			}
			else {

				! If the player has entered enough already but there's still
				! text to wade through: store the pattern away so as to be able to produce
				! a decent error message if this turns out to be the best we ever manage,
				! and in the mean time give up on this line

				! However, if the superfluous text begins with a comma or "then" then
				! take that to be the start of another instruction

				if (wn <= num_words) {
					l = NextWord();
					if (l == THEN1__WD or THEN2__WD or THEN3__WD or comma_word) {
						held_back_mode = true; hb_wn = wn-1;
					}
					else {
						for (m=0 : m<32 : m++) pattern2-->m = pattern-->m;
						pcount2 = pcount;
						etype = UPTO_PE;
						break;
					}
				}

				! Now, we may need to revise the multiple object because of the single one
				! we now know (but didn't when the list was drawn up).

				if (parameters >= 1) {
					if (parser_results-->INP1_PRES == 0) {
						l = ReviseMulti(parser_results-->INP2_PRES);
						if (l ~= 0) { etype = l; parser_results-->ACTION_PRES = action_to_be; break; }
					}
				}
				if (parameters >= 2) {
					if (parser_results-->INP2_PRES == 0) {
						l = ReviseMulti(parser_results-->INP1_PRES);
						if (l ~= 0) { etype = l; break; }
					}
					else {
						k = parser_results-->INP1_PRES; l = parser_results-->INP2_PRES;
						if (k && l) {
							if ((multi_context==MULTIEXCEPT_TOKEN && k == l) ||
								((multi_context==MULTIINSIDE_TOKEN && k notin l && l notin k))) {
								best_etype = NOTHING_PE;
								parser_results-->ACTION_PRES = action_to_be; jump GiveError;
							}
						}
					}
				}

				! To trap the case of "take all" inferring only "yourself" when absolutely
				! nothing else is in the vicinity...

				if (take_all_rule == 2 && parser_results-->INP1_PRES == actor) {
					best_etype = NOTHING_PE;
					jump GiveError;
				}

				if (multi_had > 1) {
					best_etype = TOOFEW_PE;
					jump GiveError;
				}

				#Ifdef DEBUG;
				if (parser_trace >= 1) print "[Line successfully parsed]^";
				#Endif; ! DEBUG

				! The line has successfully matched the text.  Declare the input error-free...

				oops_from = 0;

				! ...explain any inferences made (using the pattern)...

				if (inferfrom ~= 0) {
					PrintInferredCommand(inferfrom);
					ClearParagraphing(20);
				}

				! ...copy the action number, and the number of parameters...

				parser_results-->ACTION_PRES = action_to_be;
				parser_results-->NO_INPS_PRES = parameters;

				! ...reverse first and second parameters if need be...

				if (action_reversed && parameters == 2) {
					i = parser_results-->INP1_PRES;
					parser_results-->INP1_PRES = parser_results-->INP2_PRES;
					parser_results-->INP2_PRES = i;
					if (nsns == 2) {
						i = special_number1; special_number1 = special_number2;
						special_number2 = i;
					}
				}

				! ...and to reset "it"-style objects to the first of these parameters, if
				! there is one (and it really is an object)...

				if (parameters > 0 && parser_results-->INP1_PRES >= 2)
					PronounNotice(parser_results-->INP1_PRES);

				! ...and return from the parser altogether, having successfully matched
				! a line.

				if (held_back_mode) {
					wn=hb_wn;
					jump LookForMore;
				}
				rtrue;

			} ! end of if(token ~= ENDIT_TOKEN) else
		} ! end of for(pcount++)

		.LineFailed;
		! The line has failed to match.
		! We continue the outer "for" loop, trying the next line in the grammar.

		if (etype > best_etype) best_etype = etype;
		if (etype ~= ASKSCOPE_PE && etype > nextbest_etype) nextbest_etype = etype;

		! ...unless the line was something like "take all" which failed because
		! nothing matched the "all", in which case we stop and give an error now.

		if (take_all_rule == 2 && etype==NOTHING_PE) break;

	} ! end of for(line++)

	! The grammar is exhausted: every line has failed to match.
	.GiveError;

	etype = best_etype;
	if (actor ~= player) {
		if (usual_grammar_after ~= 0) {
			verb_wordnum = usual_grammar_after;
			jump AlmostReParse;
		}
		m = wn; ! Save wn so extension authors can parse command errors if they want to
		wn = 1;
		while ((wn <= num_words) && (NextWord() ~= comma_word)) ;
		parser_results-->ACTION_PRES = ##Answer;
		parser_results-->NO_INPS_PRES = 2;
		parser_results-->INP1_PRES = actor;
		parser_results-->INP2_PRES = 1; special_number1 = special_word;
		actor = player;
		consult_from = wn; consult_words = num_words-consult_from+1;
		wn = m; ! Restore wn so extension authors can parse command errors if they want to
		rtrue;
	}
	! If the player was the actor (eg, in "take dfghh") the error must be printed,
	! and fresh input called for.  In three cases the oops word must be jiggled.

	if ((etype ofclass Routine) || (etype ofclass String)) {
		if (ParserError(etype) ~= 0) jump ReType;
	}
	else {
		if (verb_wordnum == 0 && etype == CANTSEE_PE) etype = VERB_PE;
		players_command = 100 + WordCount(); ! The snippet variable "player's command"
		BeginActivity(PRINTING_A_PARSER_ERROR_ACT);
		if (ForActivity(PRINTING_A_PARSER_ERROR_ACT)) jump SkipParserError;
	}
	pronoun_word = pronoun__word; pronoun_obj = pronoun__obj;

	if (etype == STUCK_PE) { PARSER_ERROR_INTERNAL_RM('A'); new_line; oops_from = 1; }
	if (etype == UPTO_PE) {
		for (m=0 : m<32 : m++) pattern-->m = pattern2-->m;
		pcount = pcount2;
		if (inferred_go) {
			PARSER_ERROR_INTERNAL_RM('C');
			print (name) parser_results-->INP1_PRES;
		}
		else {
			PARSER_ERROR_INTERNAL_RM('B');
			PrintCommand(0);
		}
		print ".^";
	}
	if (etype == NUMBER_PE) { PARSER_ERROR_INTERNAL_RM('D'); new_line; }
	if (etype == CANTSEE_PE) { PARSER_ERROR_INTERNAL_RM('E'); new_line; oops_from=saved_oops; }
	if (etype == TOOLIT_PE) { PARSER_ERROR_INTERNAL_RM('F'); new_line; }
	if (etype == NOTHELD_PE) { PARSER_ERROR_INTERNAL_RM('G'); new_line; oops_from=saved_oops; }
	if (etype == MULTI_PE) { PARSER_ERROR_INTERNAL_RM('H'); new_line; }
	if (etype == MMULTI_PE) { PARSER_ERROR_INTERNAL_RM('I'); new_line; }
	if (etype == VAGUE_PE) { PARSER_ERROR_INTERNAL_RM('J'); new_line; }
	if (etype == ITGONE_PE) {
		if (pronoun_obj == NULL) { PARSER_ERROR_INTERNAL_RM('J'); new_line; }
		else { PARSER_ERROR_INTERNAL_RM('K', noun); new_line; }
	}
	if (etype == EXCEPT_PE) { PARSER_ERROR_INTERNAL_RM('L'); new_line; }
	if (etype == ANIMA_PE) { PARSER_ERROR_INTERNAL_RM('M'); new_line; }
	if (etype == VERB_PE) { PARSER_ERROR_INTERNAL_RM('N'); new_line; }
	if (etype == SCENERY_PE) { PARSER_ERROR_INTERNAL_RM('O'); new_line; }
	if (etype == JUNKAFTER_PE) { PARSER_ERROR_INTERNAL_RM('P'); new_line; }
	if (etype == TOOFEW_PE) { PARSER_ERROR_INTERNAL_RM('Q', multi_had); new_line; }
	if (etype == NOTHING_PE) {
		if (parser_results-->ACTION_PRES == ##Remove &&
			parser_results-->INP2_PRES ofclass Object) {
			noun = parser_results-->INP2_PRES; ! ensure valid for messages
			if (noun has animate) { PARSER_N_ERROR_INTERNAL_RM('C', noun); new_line; }
			else if (noun hasnt container or supporter) { PARSER_N_ERROR_INTERNAL_RM('D', noun); new_line; }
			else if (noun has container && noun hasnt open)  { PARSER_N_ERROR_INTERNAL_RM('E', noun); new_line; }
			else if (children(noun)==0) { PARSER_N_ERROR_INTERNAL_RM('F', noun); new_line; }
			else parser_results-->ACTION_PRES = 0;
		}
		if (parser_results-->ACTION_PRES ~= ##Remove) {
			if (multi_wanted==100) { PARSER_N_ERROR_INTERNAL_RM('A'); new_line; }
			else { PARSER_N_ERROR_INTERNAL_RM('B'); new_line; }
		}
	}
	if (etype == NOTINCONTEXT_PE) { PARSER_ERROR_INTERNAL_RM('R'); new_line; }
	if (etype == ANIMAAGAIN_PE) { PARSER_ERROR_INTERNAL_RM('S'); new_line; }
	if (etype == COMMABEGIN_PE) { PARSER_ERROR_INTERNAL_RM('T'); new_line; }
	if (etype == MISSINGPERSON_PE) { PARSER_ERROR_INTERNAL_RM('U'); new_line; }
	if (etype == ANIMALISTEN_PE) { PARSER_ERROR_INTERNAL_RM('V', noun); new_line; }
	if (etype == TOTALK_PE) { PARSER_ERROR_INTERNAL_RM('W'); new_line; }
	if (etype == ASKSCOPE_PE) {
		scope_stage = 3;
		if (scope_error() == -1) {
			best_etype = nextbest_etype;
			if (~~((etype ofclass Routine) || (etype ofclass String)))
				EndActivity(PRINTING_A_PARSER_ERROR_ACT);
			jump GiveError;
		}
	}

	.SkipParserError;
	if ((etype ofclass Routine) || (etype ofclass String)) jump ReType;
	say__p = 1;
	EndActivity(PRINTING_A_PARSER_ERROR_ACT);
	! And go (almost) right back to square one...

	jump ReType;

	! ...being careful not to go all the way back, to avoid infinite repetition
	! of a deferred command causing an error.
	! At this point, the return value is all prepared, and we are only looking
	! to see if there is a "then" followed by subsequent instruction(s).

	.LookForMore;

	if (wn > num_words) rtrue;

	i = NextWord();
	if (i == THEN1__WD or THEN2__WD or THEN3__WD or comma_word) {
		if (wn > num_words) {
			held_back_mode = false;
			return;
		}
		hb_wn = wn;
		held_back_mode = true;
		return;
	}
	best_etype = UPTO_PE;
	jump GiveError;
]; ! end of Parser__parse

-) replacing "Parser__parse".

The Darkness is A Room. The description of the darkness is "[text of the basic visibility rule response (A)]". The jewel is in the darkness.

This cuts out the inference altogether and gives the “You must supply a missing noun/second noun!“ message instead. You can then use the supplying a missing noun/second noun rules to set the relevant noun or second noun or to just print a custom rejection message.

Hope this helps!

Yes! That’s what I think should be the default.