Z-Machine: nested stream 3 calls on the same address

I’ve had a pull request to Praxix adding a test for nested stream 3 calls, and in particular calls on the same address.

Nested stream 3s are required by the 1.0 standard, and pretty essential for most Z-Code games now, but using the same address is less likely to be productive (as you’re not meant to read from the memory while a stream 3 is still open. This is not an issue for the proposed changed to Praxix as it immediately closes the stream after opening it.) But it doesn’t look to me to be a violation of the spec. Anyone else got any reason why this PR shouldn’t be accepted?

The PR author says this test fails some Z-Machine interpreters and they’ll submit patches to them if the test is approved.

1 Like

If I’m reading the spec correctly, it’s undefined behavior to read from or write to any part of the buffer while stream 3 is open; I would say opening another stream 3 pointing to any part of that buffer counts as writing to it.

I imagine it’s defined this way so that terps don’t have to worry about copying the text to Z-machine memory until the stream is closed, right? I don’t see any benefit from allowing multiple stream 3’s in the same buffer that would be worth sacrificing that optimization.

2 Likes

Yeah that makes sense!

I think I’d refuse.

There was mention of this before (I think I brought it up a long time ago).

The following bolded sentence from the standard (my emphasis) is clearly intended to mean while a stream is selected the table’s contents are entirely determined by the interpreter’s implementation. Writing to any overlapping table would therefore have to be undefined. Expecting an interpreter to catch and forbid such overlap is another question entirely…

7.1.2.1

Output stream 3 writes to a table in dynamic memory. When the stream is selected, the table may have any contents (even the initial ‘size’ word will be ignored by the interpreter). While the stream is selected, the table’s contents are unspecified (and a game cannot safely read or write to it). When the stream is deselected, the initial word of the table holds the number of characters printed and subsequent bytes hold those characters. Similarly, in Version 6, the total width of printing (in units) will then be stored in the word at $30 in the header. (It is the programmer’s responsibility to make the table large enough: the interpreter performs no overflow checking.)

1 Like

I hadn’t even thought of that part. Specifying what should happen when multiple stream 3s are writing to the same block of memory would mean that the interpreter needs to do that kind of bounds checking, right?

1 Like

The interpreter cannot do bounds checking since it doesn’t know the buffer size.

Yes and no static check will work, by definition. You can’t even just check for the same table address as ANY overlap causes the same problem.

In a nutshell - if the game code causes any overlap between stream 3 tables whatsoever: Congratulations! You have triggered undefined behavior and the interpreter is free to delete your entire game collection. :wink:

1 Like

Imagine I open stream 3 pointing to address $4000, print “123456”, open stream 3 pointing to address $4004, print “ABCD”, close stream 3, print “78”, then close stream 3. (Pathological, but legal.)

If the interpreter is writing each byte into memory as it’s printed, then we’ll get “1234AB78”. But if the interpreter is writing the characters to some internal buffer, then putting them into Z-machine RAM when the stream is closed, we’ll get “12345678”.[1]

If we specify the behavior, then one of these two optimizations is no longer legal. If we make it illegal, then the interpreter has to do bounds checking to detect this case. If we leave it as undefined behavior—which I prefer—then interpreters can do whatever is most efficient for their particular platform, and authors can be told to Just Not Do That.


  1. Well, not quite. There’s also the length byte. I’m ignoring that because this example is convoluted enough already. ↩︎

3 Likes

Well right now the spec is ambiguous, so Praxix shouldn’t be testing such overlapping buffers.

And practically, I doubt we’d get enough consensus to specify the behaviour here. So it will probably remain unspecified.

1 Like

Not quite, since the interpreter is supposed to write the length of what’s been printed into the first two bytes of the buffer.

Check Daniel’s footnote :wink:

2 Likes

Personally I think any Z code doing this is broken / buggy, so why add extra complexity to an interpreter to check for it?

These days I’m in just the peanut gallery when it comes to Z-spec questions.

However, I want to point out a third possible option, which is that this is legal but the resulting contents of the buffer are undefined. That is, we could decide that this shouldn’t crash or delete your home directory, but it’s something you shouldn’t try.

1 Like

Testing nested stream 3 calls is certainly fine, but testing nested uses of stream 3 to the same region of memory doesn’t seem particularly useful as interpreters are clearly free to do different things here.

1 Like

This area is already a bit messy.

I recently discovered that YAGWAD gives an incorrect warning on startup in Ozmoo, saying that the screen isn’t wide enough for some menu items, no matter the width of the screen. It doesn’t happen in Frotz. As it turns out, it’s because the word value for how many characters have been printed to memory is continously updated in Frotz, but it isn’t updated until stream 3 is closed in Ozmoo.