Dialog

Dialog 0e/01, library 0.20 is out. There are three small but compatibility-breaking changes to the language. They are subtly related, and I’ll end this post with an example that makes use of all three.

In addition to these changes, the release includes several bugfixes (especially in the debugger) and performance improvements.

1. It is no longer necessary to compute and declare the maximum size of global variables. From now on, any global variable can store any value, including lists. Any per-object variable can also store any value.

As a consequence, the old syntax:

(global variable (variable name $) size)

has been removed from the language.

The way this works is that a new long-term heap is shared by all global and per-object variables. This heap must be large enough to house all the complex values (i.e. lists) that are stored at any given time. By default, the compiler will allocate 500 words of long-term heap space, which should be plenty. This size can be changed with a command-line flag, -L.

(Special note for my library translator(s): Because of this change, runtime error code four has been given a slightly different meaning.)

2. Dictionary words are no longer truncated. They now have a small amount of internal structure instead: They are divided into an essential part at the beginning of the word, and an optional part at the end. The optional part is usually blank.

The location of the split is backend-specific. For the Z-machine, it is determined by the usual rules of truncation, meaning that up to nine characters are considered essential.

When two dictionary words are compared, only the essential part is taken into account. But when a word is printed, both the essential part and the optional part are printed (with no visible seam in between).

This is very useful when accepting arbitrary input as part of a grammatical statement. To invent an example, “WRITE CORNFLAKES ON SHOPPING LIST” won’t automatically lead to “On the shopping list is written: cornflake.” anymore, even if “cornflake” happens to be in the game dictionary.

When the ‘(removable word endings)’ feature is used, and the player types an unknown word that can be reduced to a known word, then the resulting dictionary word will have the known word as its essential part, and the removed ending as its optional part. Thus, in a German game where “klein” appears in the game dictionary, but “kleines” does not, the words @klein and @kleines will be considered equal during unification, but their printed representations are still “klein” and “kleines”, respectively.

In light of this change, user input is now always represented as a flat list of dictionary words and integers. It doesn’t matter if some words do not appear in the underlying Z-machine dictionary; at the Dialog level they are still represented as dictionary words. They can be stored in variables, retrieved, printed back, appear in ‘(dict $)’ rules, and so on.

As a consequence, ‘(get raw input $)’ isn’t needed anymore: To ask the player what their name is, use a regular ‘(get input $)’, which will give you a list of words. Print them back by iterating over the list and printing each word in turn, or use one of the new library predicates ‘(print words $)’ and ‘(Print Words $)’.

I have therefore tentatively removed ‘(get raw input $)’ from the language. Please let me know if you were using it for some other purpose and need it back. Likewise, ‘(print raw input $)’ has been removed from the library.

During tracing, the boundary between the essential and optional parts of a word will be marked with a plus sign (+), unless the optional part is blank. So you might see e.g. @transmogr+ify in the trace logs, which means that the word will print as “transmogrify”, but compare as @transmogr. In the same fashion, words that weren’t found in the game dictionary at all will have a plus at the end, e.g. @foo+ if the player typed the unrecognized word “foo”.

3. Resolving words to objects is considerably more efficient.

This is mostly of concern to library programmers. The old construct ‘(collect words) … (and check $)’ has been removed from the language. It is replaced by:

        (determine object $Obj)
                ... object generator ...
        (from words)
                ... word generator ...
        (matching all of $Input)

For instance:

        (determine object $Obj)
                *($Obj is in scope)
        (from words)
                *(dict $Obj)
        (matching all of $Input)

This expression backtracks over every object $Obj that makes the first inner expression succeed, and for which the second expression (when exhausted) emits at least every word in the $Input list.

Compared to the old construct, this has large performance benefits: The compiler can statically figure out what dictionary words are capable of matching what objects. So in a game with only two hats, the word “HAT” in the input will constrain the search, and ‘(determine object $Obj)’ will only backtrack over those two objects, check that they are in scope, collect their dict words, and match them against the input. On the other hand, in the unlikely case that there are hundreds of hats in the game, the object generator will be invoked first, and the full set of objects in scope (probably including just a few of the hats) will be considered.

Thanks to this optimization, a part of the standard library parser has been refactored, so that directions and objects are now parsed in the same way. Thus, complexity has been moved from the library into the compiler.

Working example: Player-named objects.

(intro)
        %% Feature 2, a name like Mr North-west won't be truncated:
        What's your name? > (get input $A)

        %% Feature 1, a per-object variable can be set to a list:
        (now) (#player has assigned name $A)

        Hello, (name #player)!
        (par)

        What's the name of your dog? > (get input $B)
        (now) (#dog has assigned name $B)

#room
(room *)
(look *)
        Here you are, with (the #dog).

#player
(current player *)
(* is #in #room)
(descr *)
        You are (name *), as good looking as ever.

#dog
(* is #in #room)
(proper *)
(descr *)
        Your best friend.

%% Feature 3, by using a trait to restrict this (name $) rule to a small set of
%% objects, we can still benefit from the optimized word-to-object lookup for
%% all other objects in the game:

(nameable #dog/#player)

(name (nameable $Obj))
        %% Feature 2, note that this code works both when printing the name
        %% of the object and when collecting the dictionary words that
        %% correspond to it. Earlier, when 'raw input' was a thing, the name
        %% would have been a list of single-character words, which wasn't
        %% compatible with the usual (dict $) mechanism.

        ($Obj has assigned name $Name)
        (Print Words $Name)

Note: As mentioned in the manual (Part I, Chapter 8, “Some notes on performance”), per-object variables tend to have a large memory footprint. In a story with hundreds of objects, of which only two are nameable, the above approach would be wasteful, and it would be better to use two global variables.

In conclusion, these changes aren’t backwards-compatible, and adapting story code to them might involve a little bit of work. But I think they are justified since they improve the lucidity, minimalism, and performance of Dialog.

1 Like