@output_stream 3 with width parameter causes interpreter halt? (Z6)

I’ve been trying to make use of the @print_form opcode within Z6. Both DM4 and the Z-Machine Standards document make reference to the fact that this opcode is well-designed for use with the contents of a table prepared via use of @output_stream, with format

@output_stream 3 <target table> <width>;

where target_table is the memory structure for the form to be printed and width is a maximum width of a line in characters (or, if negative, in screen units).

It appears that using the above is triggering an error message in Frotz 2.44: “Fatal error: Illegal window” and the same error is generated in WinFrotz 1.21. Compilation is via Inform 6.34. Is this my error, an error in the Z-Machine Standards documentation, or an error in the interpreters?

Here’s the code in use:

Constant Story "print_form test";
Constant Headline "^(the most unpopular Z-machine opcode)^";

Include "Parser";
Include "VerbLib";
Include "Grammar";


Constant FORMSIZE = 500;

Array sample_table buffer FORMSIZE;

Class Room
    has light;

Room Start "Starting Point"
    with    description
                "An uninteresting room.";

[ Initialise ;

    location = Start;

    print "<attempting direct multi-line write>^";
    @output_stream 3 sample_table;
    print "If stream is 0, nothing happens.";
    print "If it is positive, then that stream is selected; if negative, deselected.";
    print "(Recall that several different streams can be selected at once.)^";
    @output_stream -3; ! creates one entry with cumulative length of all strings

    print "<print_form follows, initial entry length = ", sample_table-->0, ">^";
    @print_form sample_table;

    print "<attempting width-constrained multi-line write>^";
    @output_stream 3 sample_table 10;    ! GENERATES "ILLEGAL WINDOW" ERROR?
    print "If stream is 0, nothing happens.";
    print "If it is positive, then that stream is selected; if negative, deselected.";
    print "(Recall that several different streams can be selected at once.)^";
    @output_stream -3; ! should create multi-entry table?

    print "<print_form follows, initial entry length = ", sample_table-->0, ">^";
    @print_form sample_table;

];

I seem to recall fixing something like this in the Frotz core. Could you try this test with the latest?

Your code isn’t right, but Windows Frotz (and Frotz 2.44) are wrong, too. From the Z-Machine specification, on @output_stream:

In Version 6, a width field may optionally be given: text will then be justified as if it were in the window with that number (if width is zero or positive) or a box -width pixels wide (if negative). Then the table will contain not ordinary text but formatted text: see print_form.

So your line of code

@output_stream 3 sample_table 10;

means “use the width of window 10”, which doesn’t exist, hence the error. However, Windows Frotz doesn’t get it right if you use a negative value for the width, which it should. This was fixed in the Frotz code code at some point after Frotz 2.44, see @output_stream handled improperly (#1) · Issues · David Griffith / frotz · GitLab

I will fix this in Windows Frotz and push out a new version, the issue for it is @output_stream handled improperly · Issue #19 · DavidKinder/Windows-Frotz · GitHub

@DavidG: Yes – the same behavior occurs with Frotz 2.53.

@DavidK: Your note about the parameter being for a window does make sense of the error message, but are you certain of that interpretation of the meaning of the parameter?

Repeating your citation from Z-Machine Standards 1.1 (The Z-Machine Standards Document):

In Version 6, a width field may optionally be given: text will then be justified as if it were in the window with that number (if width is zero or positive) or a box -width pixels wide (if negative). Then the table will contain not ordinary text but formatted text: see print_form.

and also considering the following passage in DM4 (DM4 §42: Devices and opcodes):

If width is positive, the text streamed into the array will be word-wrapped as if it were on a screen width characters wide; if width is negative, then as if on a screen -width pixels wide.

the use of “as if” phrasing in both places suggests (to me) that the width parameter is intended as a constraint on the way the opcode will print in any window.

If it is intended to get the width of a window provided as a parameter, what is the advantage over just printing directly to the window in question?

I’m not quite sure what you’re saying: as far as I can see the Z-Machine Standard is pretty clear in the meaning of “width”. The use of “as if” makes sense as we’re talking about the @output_stream opcode, so you’re printing formatted output to a memory stream “as if” you were printing to a particular window.

I think that the DM4 is simply wrong about the meaning of “width”. When it comes to the minutiae of the Z-Machine, the specification is a lot more reliable. DM4 was largely written back in 1996 or so, and the darkest corners of things like the V6 additions were not always fully illuminated. The two are certainly inconsistent in their definitions of “width”: both cannot be right.

I’m (perhaps erroneously) thinking of this functionality for @output_stream as being intimately paired with the functionality of @print_form, as both DM4 and ZMS reference @print_form specifically when describing use of the width parameter. Per the ZMS entry for @print_form (The Z-Machine Standards Document):

Prints a formatted table of the kind written to output stream 3 when formatting is on. This is an elaborated version of print_table to cope with fonts, pixels and other impedimenta. It is a sequence of lines, terminated with a zero word. Each line is a word containing the number of characters, followed by that many bytes which hold the characters concerned.

I’m not entirely sure what “word-wrapped” (DM4) or “justified” (ZMS) even means in the context of “printing” to an array in memory, but the sense that I made of it was that each virtual line would be transcribed as what @print_form will consider as an “entry” in the table to be printed.

Thus, the “as if” would mean that the maximum length of a line/entry would be the specified width such that, when the resulting array is printed via @print_form, the text produced would have a layout akin to what would be printed in a window of the given width – even if the window in which it was printed was a different size. The author would be free to match the width parameter supplied to @output_stream to the width of a particular window, but it would not be necessary to use a window as the specification for the target width. (And it’s not clear why the author would want to match the width of a pre-made window; see below.)

However, my starting point for understanding this stuff was the citation in DM4, which I trusted to be correct. It wouldn’t be the first time that what’s stated there turned out to not be reflected in the code, but I’m having trouble understanding the utility of the implementation that you’re describing. It sounds like the imagined workflow would be:

  1. decide that it’s desirable to print a column of constrained width
  2. set up a window X with the target width
  3. use window X as the width parameter to @output_stream
  4. use @print_form to output the contents of the buffer to the screen (in some window, not necessarily X)

If this is the design, then it seems more natural (to me) to print directly to the window set up in step 2, making use of @output_stream and @print_form superfluous. The workflow I had imagined (again, primed by DM4) was:

  1. decide that it’s desirable to print a column of constrained width
  2. use the target width as the width parameter to @output_stream
  3. use @print_form to output the contents of the buffer to the screen

That matches the use case of, say, printing a newspaper story in a narrow column format.

Perhaps it’s just a matter of me not understanding the intended use case of the way that it’s working now. Can anyone provide an example? (Does anyone know anything about the two uses to which this opcode is put in Infocom’s library, as cited ZMS Appendix E?)

I don’t think you’re wrong, it’s just that @output_stream supports both the workflows you describe. If you want the resulting array to be formatted as if it were printed to (say) window 2, you would use

@output_stream 3 sample_table 2;

But if you wanted the array to be formatted for a width of 100 pixels, you have to use:

@output_stream 3 sample_table -100;

Except that that doesn’t work in Windows Frotz because of the bug mentioned previously. Now you could say that that doesn’t seem like a well designed interface, and I would not disagree with you: as people have pointed out before, Infocom’s V6 additions to the Z-Machine look like an ad-hoc mess of opcodes added as they were needed, with no underlying logic or organization. Since you can always look up the width of a window, it would seem much simpler if the “width” argument always meant “this many pixels”.

As for what use it is to specify the width not as pixels but in reference to a window, I don’t know. Possibly there are examples of it in the extant Infocom V6 sources. Or possibly that is the behaviour as implemented in Infocom’s Z-code interpreters. But that’s what we’re stuck with, at least if you want to use V6.

That’s something that I had missed; I was thinking in terms of the minus sign as simply specifying screen units instead of characters, with the underlying functionality otherwise the same. Thank you for pointing it out.

Now I’m very curious about the operation of this functionality in the wild.

Given this, can we conclude that @otistdog’s sample code is malformed and thus the halt/crash is correct?

I remain skeptical of the definition found in ZMS 1.1 for the reasons outlined above. Even though @DavidK’s interpretation of the opcode’s entry is reasonable as a strictly literal interpretation of the wording, I am doubtful that the wording in the standard is reflective of the opcode’s actual operation. (Since I have no idea of where to find the only 2 known instances of Infocom’s usage of this opcode – and wouldn’t know how to read the corresponding ZIL anyway – I can’t follow up on that hunch.)

In the meantime, use of a negative width parameter in combination with header block data about character width in screen units lets me accomplish what I was trying to do in Inform 6.

Yes, that is correct.

We can at least make a start. Using Inform vs ZIL names for Z-code opcodes we find that the ZIL name for @output_stream is “DIROUT”. Searching through the V6 Infocom ZIL source code at The Infocom Files · GitHub I can’t find any actual uses of DIROUT with three arguments at all. The closest is this, which occurs in several places in “verbs.zil” in Zork Zero:

<DIROUT ,D-TABLE-ON ,SLINE ;-80>

The semi-colon indicates a comment, but it’s interesting that that “-80” is there at all, suggesting a partially implemented or abandoned feature.

The definitions of the opcodes were found by a combination of studying Infocom’s Z-code files and disassembling Infocom’s interpreters. Some of these obscure corner cases in opcodes were implemented in interpreters but used in no (or only one) version of the released Infocom story files. It looks like the third argument to @output_stream is one of these cases. The use of a negative number for the width in pixels suggests that the first version of the three argument implementation used positive values for something else (and “specify a window” would be the obvious explanation) then when that turned out to be insufficient and “specify a width” was needed, it was just hacked in using negative numbers.

Further digging: since the Z-Machine specification was written, the original Infocom YZIP specification has become available, there’s a PDF transcript of it here: https://raw.githubusercontent.com/heasm66/YZIP-Specifications/master/yzip%20specifications.pdf. From the bottom of page 37:

DIROUT 3 is more complex if the just argument is supplied. In that case, just is either a windownumber, or the negative of a width in pixels. Output to the table will be justified as though it was being sent to the window or to a window just pixels wide. In either case, the result is a “formatted” table, suitable for passing to PRINTF. The way this is accomplished is to wrap output at the width given, padding it if necessary. Each time a line is filled, the number of characters output is PUT in offset 0 of the table’s last line, the count is reset to zero, and ZIP skips a word to use as the byte count for the next line. When a DIROUT -3 is performed, the last line is padded, its count is PUT, and a word of zero (meaning an empty line) is PUT.

I was just about to point to the YZIP documentation for the Infocom description of DIROUT. But you beat me to it…

Well, that settles that! Much obliged to @DavidK for the additional research on top of the informative answer above. It is very illuminating, and I particularly appreciate the Inform/ZIL translation table.

I’ve been thinking about this some more. If I’m understanding you correctly, there is not one single instance “in the wild” of the width parameter being positive in Infocom-produced works.

If that’s the case, then given that the opcode’s functional description is basically nonsensical for this case, what would be the harm of changing it to work more in line with naive expectations (i.e. just as a plain width parameter instead of a window ID number)?

Well, not quite. We have the ZIL source code of the Infocom games at the time of Infocom’s final demise, and there doesn’t seem to be a three-argument call to DIROUT in any of that. But the various different versions of the V6 games released for different platforms were built from slightly different versions of that code at different times, so it is possible that such calls are out there in those files.

That gets us into all sorts of problems, both practical and philosophical. If you wanted to make such a change, you’d have to persuade all the authors of the various interpreters to implement that change. Even if you could do that, what do you do about people using older versions of existing interpreters? So then you end up adding a flag to say whether the interpreter supports the new behaviour or not, and then games end up having to test the flag, and do things in one way or another based on that, making the game code even more complicated than the original problem you’re trying to solve.

So I would say no, there’s no chance of being able to make that change now. Especially as the specification already gives you the functionality you want, just in a slightly awkward way.

There’s also the philosophical point that we’ve never deviated from Infocom’s specification before. We’ve added to it, sure, but never said “we’re doing this, which is different to what Infocom did”.

1 Like

I should have been clearer about the above: I meant why not change the behavior in a future version of the specification; your points about unilaterally changing any one interpreter are well-taken, and it was not my intent to suggest that be done.

In the context of a changed specification (and the possibly incorrect assumption that there is zero existing usage of the positive width version of the opcode), the practical answers are:

  • persuading interpreter authors - It’s an elective matter to comply with the new specification or not, much as the change from pre-spec to 1.0 or from 1.0 to 1.1.

  • people using older interpreters - If the opcode doesn’t occur in the wild, then there’s no potential of harm. If they want to play a game that depends on a newer specification’s definition, then they can upgrade their interpreter – comparable to the situation of obtaining a bug-fix release.

  • complicating interpreters - The idea was that there is no reason to retain the existing function at all, so no necessity of flags, etc. Why support an unused and non-useful behavior?

The philosophical questions are deeper. I love Infocom plenty, but I question the value of preserving what very much looks like a flaw in the design of the specification. To quote ZMS:

Where Infocom’s own shipped interpreters disagree, or contain manifest bugs, it has usually been possible to decide which was “correct”. Elsewhere, minimum levels of performance have been invented where necessary.

The current specified behavior for the positive width version of the opcode would count as a “manifest bug” situation at the design level, in my book.

At any rate, thank you again for pointing out the practical workaround. I truly appreciate it, as well as your engagement on these questions.

A final comment on this: I was wrong to say that Windows Frotz 1.21 gets this wrong. Having checked the code, Windows Frotz does do the right thing, after a patch (following the one for Frotz) was added back in 2015.