Chapter 7: More built-in predicates

Checking the type of a value

Dialog contains a set of built-in predicates for checking if a value is of a particular type. They are:

(unbound $X)

Succeeds if and only if $X is currently an unbound variable.

(number $X)

Succeeds if and only if $X is bound to a number.

(word $X)

Succeeds if and only if $X is bound to a dictionary word.

(empty $X)

Succeeds if and only if $X is bound to an empty list.

(non-empty $X)

Succeeds if and only if $X is bound to a non-empty list.

(list $X)

Succeeds if and only if $X is bound to a list (empty or non-empty).

The last one comes with an extra feature:

(object $X)

If $X is bound to an object, the query succeeds. If it is bound to anything else, the query fails. But if $X is unbound, *(object $X) backtracks over every object in the game.

Numbers and arithmetic

The Dialog language is designed for symbolic manipulation, predicate logic, and storytelling. Arithmetic is possible, but the syntax is rather clunky.

($A plus $B into $C)

A and B must be bound to numbers; C is unified with their sum. If the result is outside the valid range of numbers, the query fails.

($A minus $B into $C)

A and B must be bound to numbers; C is unified with their difference. If the result is outside the valid range of numbers, the query fails.

($A times $B into $C)

A and B must be bound to numbers; C is unified with their product. If the product is outside the valid range of numbers, the query succeeds, but the numeric result is unpredictable (i.e. it depends on the interpreter).

($A divided by $B into $C)

A and B must be bound to numbers; C is unified with the (integer) quotient after dividing A by B. The query fails if B is zero.

($A modulo $B into $C)

A and B must be bound to numbers; C is unified with the remainder after dividing A by B. The query fails if B is zero.

(random from $A to $B into $C)

A and B must be bound to numbers, such that B is greater than or equal to A. A random number in the range A to B (inclusive) is picked, and then unified with C.

($A < $B)

This predicate succeeds if and only if A is numerically less than B.

($A > $B)

This predicate succeeds if and only if A is numerically greater than B.

Common to all of the above predicates is that they fail if A or B is unbound, or bound to a non-number. C may be bound or unbound; it is unified with the result of the computation.

To check for numerical equality, use regular unification, i.e. ($ = $).

All numbers in Dialog are restricted to the range 0–16383 (inclusive). This range directly supports four-digit numbers such as years and PIN codes. Pocket money should be fairly straightforward to implement by counting in cents; story authors (or library developers) that require more sophisticated number crunching will have to get creative.

Styles and formatting

To change the current text style, use the built-in predicates (bold), (italic), (reverse), and (fixed pitch) to enable each respective style, and (roman) to disable all four.

The built-in predicate (uppercase) forces the next character to be printed in uppercase. The standard library uses this to define a number of convenient predicates such as:

(The $Obj)(uppercase) (the $Obj)
[Copy to clipboard]

To clear the screen below the status bar area, use (clear). To clear the entire screen and disable the status bar, use (clear all). Be aware that on some interpreters, clearing interferes with the player's ability to scroll back and review earlier parts of the play session.

The built-in predicate (space $) prints a given number of space characters in succession.

Its vertical counterpart, (par $), produces a paragraph break with a height corresponding to the given number of blank lines. Adjacent paragraph breaks are merged based on the maximum specified height, so (par 5) (par 7) produces seven blank lines.

(program entry point)
(clear all)
(par 10)
(space 10)
(bold)
This text is printed in bold, starting at row 11, column 11.
[Copy to clipboard]

Input

User input is represented by dictionary words.

The Dialog compiler collects all dictionary words mentioned explicitly in the source code (with the @-prefix or as bare words inside lists), as well as every literal word that can come out of a (collect words) or (determine object $) expression. In addition, the system makes sure to provide a single-letter dictionary word for every character supported by the underlying platform. Together, these words make up what's called the game-wide dictionary.

It may be helpful to know that there's a difference between dictionary words at the Dialog level, and the native, low-level words of the Z-machine. Dialog dictionary words are an abstraction over several different kinds of internal representation. That being said, it is the specific constraints of the low-level Z-machine dictionary that determine where the split occurs between the essential and optional parts of a given dictionary word.

There are two built-in predicates for obtaining input from the player. One waits for a single keypress, while the other reads a full line of input.

Get key

(get key $Char)
[Copy to clipboard]

This predicate waits for the player to type a character.

Some interpreters indicate that the game is waiting for input by displaying a flashing cursor. Others don't, so story authors may wish to prompt the reader explicitly.

The parameter, $Char, is unified with a dictionary word representing the character that was typed, e.g. @e if the E key was pressed. Note that dictionary words are case-insensitive, so for letters of the alphabet there is no way to tell whether the player was holding shift or not.

Some special keys correspond to dictionary words that can't be represented directly in Dialog source code. The dictionary words for these keys can be obtained at runtime, by querying the following built-in predicates:

(word representing return $Char)
(word representing space $Char)
(word representing backspace $Char)
(word representing up $Char)
(word representing down $Char)
(word representing left $Char)
(word representing right $Char)
[Copy to clipboard]

A simple keypress dispatcher might look like this:

(program entry point)
(get key $Key)
(handle keypress $Key)
(handle keypress @a)
'A' was pressed.
(handle keypress @b)
'B' was pressed.
(handle keypress (word representing return $))
RETURN was pressed.
[Copy to clipboard]

Get input

(get input $WordList)
[Copy to clipboard]

This query blocks execution until the player types a line of input, followed by return. Different interpreters provide different levels of line-editing facilities, ranging from simple backspace handling all the way up to input history and spell checking.

The parameter, $WordList, is unified with a list where each element represents a word typed by the player. The punctuation characters full stop, comma, double quote, semicolon, and asterisk are treated as individual words; the remaining text is separated into words by whitespace. If a word is recognized as one that appears in the program-wide dictionary, then the element will be that dictionary word. Else, if the word is a decimal number in the range 0–16383, the element will be that number.

If the word was neither recognized, nor found to be a decimal number, then Dialog will attempt to remove certain word endings, and check whether the remaining part of the word exists in the dictionary. This procedure is necessary for games written in e.g. German, whereas English games generally do not require it.

To specify removable endings, add one or more rule definitions to the predicate (removable word endings). Each rule body should consist of one or more word endings:

(removable word endings)
%% German adjective endings
en es em e
(removable word endings)
%% German noun endings
e en s es
[Copy to clipboard]

The part that remains after removing the ending is referred to as the stem of the word. If the stem consists of at least two letters, and exists in the program-wide dictionary, then the resulting dictionary word will have the stem as its essential part, and the ending as its optional part. During comparison (unification with another bound value), only the essential part is considered. During printing, both the essential part and the optional part are printed.

During tracing, dictionary words are displayed with a plus sign (+) separating the essential and optional parts. Thus, if the German word “klein” is part of the game-wide dictionary, and the player enters KLEINES, that word appears as @klein+es in the trace logs, and unifies successfully with @klein.

If a word of input isn't recognized at all, even after considering the removable word endings, then it's an unrecognized dictionary word. It can still be stored in a variable, retrieved, and printed back, and it will unify successfully with other instances of the same unrecognized word. When tracing is enabled, unrecognized dictionary words are displayed with a plus sign at the end.

For instance, the input TAKE 02 UNKNOWNWORD,X BALL may, depending on the contents of the dictionary, be represented by the list: [take 2 unknownword , x ball]. As part of a trace, it might be displayed as [take 2 unknownword+ , x ball].

Special gotcha: Recall that zero-prefixed numbers in the source code, as well as numbers that are out of range, are treated as words. If 007 appears in the program in such a way that it becomes part of the program-wide dictionary, then it will show up as a dictionary word in the list returned by (get input $). Otherwise, it will be represented by the numerical value 7.

Splitting input by keywords

During parsing, it is often necessary to scan a list for certain keywords, and then split it into two sublists, representing the elements on either side of the matched keyword. It is straightforward to implement this using ordinary rules in Dialog. However, for performance reasons the language also provides a built-in predicate:

(split $Input by $Keywords into $Left and $Right)
[Copy to clipboard]

$Input and $Keywords must be lists of simple values, i.e. they mustn't contain sublists.

The $Input list will be scanned, starting at its head, until the first element that also appears in $Keywords is found. A list of the elements that came before the keyword is unified with $Left, and a list of the elements that follow it is unified with $Right. That is, neither $Left nor $Right includes the keyword itself.

When invoked as a multi-query, the predicate backtracks over each matching position. Thus:

*(split [the good , the bad and the ugly]
by [and ,]
into $Left and $Right)
[Copy to clipboard]

will succeed twice: First, binding $Left to [the good] and $Right to [the bad and the ugly], and then a second time binding $Left to [the good , the bad] and $Right to [the ugly].

The split-by predicate can also be used to check whether a list contains one or more of a set of keywords. The standard library uses it that way in the following rule definition:

($X contains one of $Y)
(split $X by $Y into $ and $)
[Copy to clipboard]

Determining objects from words

This section is mainly of concern for library programmers, so story authors may safely skip it.

Dialog has a special construct for matching player input against in-world object names in a very efficient way:

(determine object $Obj)
object generator
(from words)
word generator
(matching all of $Input)
[Copy to clipboard]

This statement will backtrack over every object $Obj for which:

object generator succeeds, and
word generator, when exhausted, emits at least every word in the $Input list.

The variable $Obj should appear both in the object generator and in the word generator, and the object generator should contain a multi-query to backtrack over a set of objects. A canonical example is:

(determine object $Obj)
*($Obj is in scope)
(from words)
*(dict $Obj)
(matching all of $Input)
[Copy to clipboard]

A non-optimizing compiler could deal with this construct as follows: First, compile normal code for the object generator. Then, exhaust every branch of the word generator, collecting all its output into a temporary list of words. Finally, check that every word of $Input appears in the temporary list.

However, the Dialog compiler and debugger both perform the following optimization: At compile-time, they analyze the word generator statically, and construct a reverse lookup table, mapping words of input to objects. At runtime, this table is consulted first, based on $Input, to determine what objects the player might be referring to. So, for instance, if the input is LARGE HAT, and there are only two game objects for which (dict $Obj) can produce both of those words, then $Obj will now be bound to each of those two objects in turn. But if there are dozens of large hats, $Obj may instead be left unbound; the compiler is allowed to make a trade-off between speed and memory footprint. Either way, after this step, the operation proceeds as in the unoptimized case.

Debugging

The following built-in predicates are useful for debugging:

(breakpoint)

If the program is currently running inside the interactive debugger, suspend execution and print the current source code filename and line number. When execution resumes, this query succeeds.

If the program is running on the Z-machine, the query simply succeeds.

(trace on)

Enables tracing. Following this, debugging information will be printed when queries are made, and when rule bodies are entered. The interactive debugger will also report when solutions are found, and when dynamic predicates are updated.

(trace off)

Disables tracing.

If your program source code contains a query to (trace on) anywhere, the Z-machine backend will insert extra instructions all over the generated code, to deal with tracing. This is known as instrumenting the code, and it makes the program slower and larger. Thus, you'll only want to use these predicates temporarily, during debugging. The compiler prints a warning when it adds the extra instructions.

Please be aware that the Dialog compiler and debugger do optimize your program, and you will be tracing the optimized code, so certain queries and rules will be missing from the debug printouts. You will generally want to do all your tracing in the debugger, which mercifully turns off some of the more confusing optimizations. That being said, tracing the optimized Z-code is highly useful when trying to speed up a program.

System control

The following built-in predicates offer low-level control over the interpreter and the Dialog runtime. This is decidedly in the domain of library code, so story authors rarely need to worry about these predicates.

(quit)

Immediately terminates the program. This predicate neither fails nor succeeds.

(restart)

Resets the program to its initial state. The only part of the game state that may survive a restart is the state of the output machinery (including the current style and on-screen contents, and whether the transcript feature is on or off). If the operation succeeds, execution resumes from the start of the program. If there is an error, or the interpreter doesn't support restarting, execution continues normally, i.e. the query succeeds.

(save $ComingBack)

Attempts to save the current game state to a file. The interpreter takes care of asking the player for a filename. In the event of a save error, or if the operation was cancelled, the query fails. On success, the parameter is unified with 0 if we just saved the state, and with 1 if we just restored the state from a file saved by this query.

(restore)

Attempts to restore the current game state from a file. The interpreter takes care of asking the player for a filename. The only part of the game state that may survive a restore is the state of the output machinery (including the current style and on-screen contents, and whether the transcript feature is on or off). If the operation succeeds, execution resumes after the query from which the save file was written. Otherwise, in the event of a load error or if the operation was cancelled, execution continues normally, i.e. the query succeeds.

(save undo $ComingBack)

Works just like (save $), but stores the game state in a buffer in memory. This operation is typically invoked once per move.

(undo)

Works like (restore), but restores the game state from the undo buffer. If there is no saved undo state, the predicate fails. If there's some other problem—such as the interpreter imposing a limit on the number of undo states that are retained in memory—the predicate succeeds, and execution continues normally.

(interpreter supports undo)

Succeeds if and only if the current interpreter declares that it supports undo functionality.

(script on)

Enables the transcript feature. The interpreter takes care of asking the player for a filename. If the operation succeeds, the query suceeds. In case of an error, or if the operation was cancelled, the query fails.

(script off)

Disables the transcript feature. This predicate always succeeds.

(display memory statistics)

Prints a line of information specific to the compiler backend, about the peak memory usage in the heap, auxiliary heap, and long-term heap areas. Currently, only the Z-machine backend does this. The size of these areas can be adjusted by passing commandline options to the compiler. During debugging and testing, you may wish to invoke this predicate just before quitting, as it will tell you how close you are to the limits.