[I6] Circular dependencies for LanguageToInformese() and global wn (affecting DM4 Ex 111)?

The provided example solution code for DM4 Ex 111 (pages 502-503, or https://inform-fiction.org/manual/html/sa6.html#ans110 and https://inform-fiction.org/manual/html/sa6.html#ans111 ; solutions are cumulative) seems to have a circular dependency, in that:

  1. Parser section A uses #Ifdef to invoke LanguageToInformese() only if it is defined.
  2. The example solution to DM4 Ex 111 makes use of global variable wn, which is defined in parser.h.

The combination seems to make it impossible to compile in a way that lets it actually function, i.e. it can be successfully compiled with a LanguageToInformese() that uses wn and is placed after the inclusion of parser.h, but in that case it will not be invoked.

Is there some way out of this deadlock?

IIRC, you can declare:

Replace LanguageToInformese;
Stub LanguageToInformese 0;

before you include parser.h, and then provide the real definition afterwards. (I suspect you’ll also need to include english.h first before actually implementing it.)

Having said that, it looks like ex 111 also leaves wn set to a value other than 1, which I don’t think Parser A is expecting either. Quite likely, this whole thing is a “don’t do that”.

It should be sufficient to set wn back to 1 at the end of the function.

@mirality, thank you! Stub is not explained until the next section of the DM4, but it will indeed do the trick.

Also, FYI – I took a peek at that section of parser.h (Std Lib 6/11 version from Inform 6.31), and, following the call to LanguageToInformese(), verb_wordnum is set to 1 (shortly before .AlmostReparse), then wn is set to be equal to verb_wordnum (just after .BeginCommand), so perhaps LanguageToInformese() is one of the entry points where you don’t have to care about where you leave wn?

There’s something a little different going on in the StdLib 6.12.4 version, though… if BeforeParsing() returns false, then LibraryExtensions.ext_number_1 will be set to the value of wn as it was left by LanguageToInformese(). The comment for this reads “! Set 'between calls' functionality to restore wn each pass”, so perhaps leaving wn in a different state could be a problem in the current version. (It’s hard to tell. The parser routine is still mostly unexplored territory for me, so I’m not following the logic of what it’s doing.)

BeforeParsing is another stub that you could probably override instead of LanguageToInformese – the main difference is that it doesn’t do the automatic re-parse (but you can do that yourself if needed), and the return value controls whether it runs the “before parsing extensions” or not.

BeforeParsing itself is covered in DM4, but the extensions seem to be a new thing. From context it looks like a way to add multiple “hooks” so that multiple included extension files can co-exist without stomping on each other, in a manner somewhat like I7 extensions. Usage is described here.

@mirality, I spoke too soon, unfortunately; I’m not sure that it does work to use the Stub directive. Placing the stub prior to the inclusion of parser.h does (presumably) make the parser invoke the LanguageToInformese() routine, but I haven’t found any way to replace the stub-generated routine later – once it’s defined, it’s defined. (I keep forgetting that Inform 6.31 does not let one redefine functions. The ability to redefine routines that was introduced in 6.34 would help a lot here, but it makes me wonder how the example solution could have been made to work in the 6.21 era when DM4 was released.)

For 6.31, it looks like the best (and only?) bet is to create a modified english.h file and compile with the +language_name="<modified file name>" switch. (Details are not in DM4, only in the header at the top of english.h and – indirectly – in the comments for the Include "language__"; statement in parserm.h.)

Did you try simply omitting the Stub definition? I’m not sure if that’s actually needed, using Replace alone might be sufficient. (I don’t have a compiler handy to verify this myself.)

If that doesn’t work, you could place the Stub (and only that) in another custom file that is also marked with System_file; and included before parser.h.

Otherwise, as previously noted, the following should behave exactly equivalent to implementing LanguageToInformese, so it could be used as an alternative:

Replace BeginParsing;

! all the includes go here

[ BeginParsing;
    ! whatever parsing code you like goes here

    #IfV5;
    ! Re-tokenise:
    Tokenise__(buffer,parse);
    #Endif; ! V5

    wn = 1;
    rfalse;
];

The only use of wn in that example is as an “argument” to the NextWord() function. You could avoid the whole problem by rewriting NextWord() in place, or adding a helper function elsewhere in the code:

[ NextWordArg val   oldwn res;
  oldwn = wn;
  wn = val;
  res = NextWord();
  wn = oldwn;
  return res;
];

This is entirely clunky and should have been fixed when the example was written. (Or maybe the compiler changed out from under the example?) But it gets the job done. A lot of IF coding winds up at “got the job done”.

First: The provided example solution in DM4 is partial, and the provided block must be inserted before the for loop from the Ex 110 solution, or the call to LTI_Insert() in the Ex 110 loop will invalidate the word location information returned by WordAddress() in the Ex 111 block. Local variables added by the new block (word and at) must be added to the routine declaration.

Second: The use of local variable at in the commands intended to replace characters in the player input is incorrect. WordAddress() returns a memory location; this value is far too large to be used as an index against the buffer array, so the code as provided in the example solution causes RTEs. This can be fixed by changing the assignment of at to:

at = WordAddress(x) - buffer;

and adjusting calls to LTI_Insert() to:

LTI_Insert(at, ' ');

Perhaps these should be added to the list of DM4 errata. Is there a more complete list than the one at https://inform-fiction.org/patches/misprints.html ?

Third: To break the circular dependency, zarf’s solution of setting up a helper function definitely does get the job done. (Thanks, zarf!) To use it, change line:

wn = x; word = NextWord();

to

word = NextWordArg(x);

and adhere to the following order of directives and definitions in the source code:

  1. Replace LanguageToInformese; ! causes LanguageToInformese() in english.h to be ignored
  2. [ LanguageToInformese ... ]; ! defines routine, hiding dependency on wn via indirect usage
  3. Include "Parser"; ! causes parser to be compiled to invoke LanguageToInformese(), because routine is defined
  4. [ NextWordArg ... ]; ! OK to make use of wn because compiler has seen its declaration in parser code

Finally: it’s not easy to tell whether or not this routine is working without playing back the contents of buffer. The following routine does that:

[ ShowCurrentCommand   linelen i;
    linelen = buffer->1;  ! characters parsed, see DM4 p. 44
    print "^MODIFIED COMMAND: ";
    for (i=0: i<linelen: i++)
            print (char) buffer->(i+2);
    print "^";
];

A call to this can be added at the end of LanguageToInformese() to show the post-modification contents of buffer.

Here’s a complete working example:

(compiles and works under Inform 6.31)

Constant Story “DM4 Exercise 111b (example solution)”;
Constant Headline “^from p. 268^”;

Replace LanguageToInformese;

[ LanguageToInformese x word at ;
! Insert a space before each hyphen and after each apostrophe.
for (x=1: x<=parse->1: x++) {
word = NextWordArg(x); ! THIS LINE MODIFIED FROM DM4
at = WordAddress(x) - buffer; ! THIS LINE MODIFIED FROM DM4
if (word == ‘dessus’) {
LTI_Insert(at, ’ '); ! THIS LINE MODIFIED FROM DM4
buffer->at = ‘s’; buffer->(at+1) = ‘u’; buffer->(at+2) = ‘r’;
buffer->(at+3) = ’ '; buffer->(at+4) = ‘l’; buffer->(at+5) = ‘u’;
buffer->(at+6) = ‘i’;
break;
}
if (word == ‘dedans’) {
LTI_Insert(at, ’ '); ! THIS LINE MODIFIED FROM DM4
LTI_Insert(at, ’ '); ! THIS LINE MODIFIED FROM DM4
buffer->at = ‘d’; buffer->(at+1) = ‘a’; buffer->(at+2) = ‘n’;
buffer->(at+3) = ‘s’; buffer->(at+4) = ’ '; buffer->(at+5) = ‘l’;
buffer->(at+6) = ‘u’; buffer->(at+7) = ‘i’;
break;
}
}
for (x=2: x<2+buffer->1: x++) {
if (buffer->x == ‘-’) LTI_Insert(x++, ’ ‘);
if (buffer->x == ‘’’) LTI_Insert(++x, ’ ‘);
}
#ifdef DEBUG;
if (parser_trace >= 1) {
print “[ After LTI: '”;
for (x=2: x<2+buffer->1: x++) print (char) buffer->x;
print "’]^";
}
#endif;
ShowCurrentCommand(); ! THIS LINE ADDED; optional, see below
];

Include “Parser”;
Include “VerbLib”;
Include “Grammar”;

[ NextWordArg val oldwn res ;
oldwn = wn;
wn = val;
res = NextWord();
wn = oldwn;
return res;
];

[ ShowCurrentCommand linelen i; ! plays back contents of buffer to see changes
linelen = buffer->1; ! characters parsed, see DM4 p. 44
print "^MODIFIED COMMAND: ";
for (i=0: i<linelen: i++)
print (char) buffer->(i+2);
print “^”;
];

Class Room
has light;

Room Start “Starting Point”
with description
“An uninteresting room.”;

[ Initialise ;

location = Start;

];

Test commands like >DONNE-LUI L’OISEAU and >METTEZ L’EAU DESSUS are transformed as required by the exercise instructions.

The above process is simplified somewhat by creating an alternate language file (i.e. a modified version of english.h) and replacing the definition of LanguageToInformese() directly within it, then telling the compiler to substitute that file using the +language_name="<modified file name>" switch, as explained in the header of Standard Library file english.h. (Although this switch is not covered in DM4, the text does suggest that the reader inspect the contents of english.h near the start of the section on p. 258 [https://inform-fiction.org/manual/html/s36.html]). As far as I can tell, this approach must be used for Exercises 115 through 118 on pages 272-273 of the following section 37 [https://inform-fiction.org/manual/html/s37.html], as there is no way to modify the size of the relevant arrays LanguageGNAsToArticles and LanguageArticles once they have been defined.

Hopefully, all of the above will help someone else working through DM4 to be less confused than I was by this exercise!

@mirality, omitting the stub means that the parser will be compiled to not invoke the LanguageToInformese() routine, because inclusion of the invocation is conditional to the routine having been defined.

Putting the Stub directive in another file marked as a system file does not help, for the same basic reason. As far as I can tell, the Replace directive is obeyed and the stub function does not get defined, but that means that the parser is compiled to omit the call to LanguageToInformese(). (This looks functionally the same as just having a Replace directive; a replacement function placed after inclusion of parser.h is compiled but never called.) Even if the Replace directive were not obeyed, the problem would then become that the stub version had been defined (and could not be subsequently redefined in 6.31).

As you noticed, the handling of BeforeParsing() is different – the call to that routine is invariably included when the parser is compiled in Standard Library 6/11. And the call to BeforeParsing() is in very close proximity to where LanguageToInformese() goes if it is included. So using BeforeParsing() is definitely another option for the general case of wanting to make use of wn at that stage of the parsing in Inform 6.31 (with the caveat you mentioned about introducing the need to retokenize after making changes to buffer). I was focused on LanguageToInformese() because that’s the topic of Ex 111, but it’s useful to see how these hooks get used in the parser machinery. Thanks for pointing it out, and for directing my attention to the release notes on StdLib 6/11’s LibraryExtensions.