Advent of Code in Dialog
Not long after I published the article on programming the Z-machine using plain
Inform 6 with no standard library, someone let me know I might like the Dialog
language. What’s really cool about Dialog is that it takes the evaluation model
of Inform 7, improves on it by being more consistent about it, and wraps it in a
sensible syntax.1 Given the description, you might suspect that it’s a higher
level language than Inform 6, which is true. For that reason, it will also not
compile to version 3 of the Z-machine, but it does compile to version 5. I have
discovered during the process of making a version 3 game that players really
don’t like version 3. It’s too limited for modern sensibilities. In particular,
the lack of an UNDO command annoys players.
I have toyed around with Dialog a little, and decided to try it on this year’s Advent of Code. Unfortunately I got knocked out in the flu halfway through the second day, and then I had to catch up with other things, so there won’t be too many solutions here. The flu is also the excuse for why the code for the second day isn’t refactored to look better, and why the presentation in this article isn’t as polished as it could be.
The more valuable part of this article, perhaps, is what’s below the code for this year: a brief tutorial^Wmy notes from learning the language, by warming up on an earlier Advent of Code problem.
Day 1, 2025
The first half of this day is about implementing modular arithmetic and detecting when operations result in zero. Dialog is really not good at maths – it doesn’t have negative numbers – so we’ll have to use a workaround and implement subtraction as addition.
%% We will need to keep track of the number %% of zeroes as well as increase them. (global variable (zeroes 0)) (incf-zeroes) (zeroes $Some) ($Some plus 1 into $More) (now) (zeroes $More) %% The main loop gets input and processes it. (program entry point) *(repeat forever) (get input $Words) (process $Words) %% Process the empty input by printing the %% number of zeroes found, then returning %% successfully. (process []) (zeroes $Some) Found $Some zeroes. %% Process instructions by splitting into %% direction and number, then performing the %% indicated rotation. (process [$Instruction | $]) (split word $Instruction into [$Direction | $Numbers]) (join words $Numbers into $Amount) (perform-rotation $Amount $Direction) %% Perform a rotation on the global position %% variable, increasing the zero count if it %% lands on a zero. (global variable (position 50)) (perform-rotation $Amount $Direction) (position $Current) (rotate $Current $Amount $Direction $Next) (now) (position $Next) { ($Next = 0) (incf-zeroes) } (fail) %% Left rotation. Find the actual effect by %% stripping away any full turns, and then %% implement the actual effect by returning %% the result of the equivalent right rotation. (rotate $Current $Amount @l $Next) ($Amount modulo 100 into $Actual) (100 minus $Actual into $Opposite) (rotate $Current $Opposite @r $Next) %% Right rotation. Find the actual effect by %% stripping away any full turns, and then %% return the result of that effect. If it %% still overshoots, first figure out where %% it lands. (rotate $Current $Amount @r $Next) ($Amount modulo 100 into $Actual) ($Current plus $Actual into $Result) (if) ($Result > 99) (then) ($Result minus 100 into $Next) (else) ($Next = $Result) (endif)
In the second half, we additionally need to keep track of when we pass zero as
part of any operation, not just when zero is the result of an operation. At
first this seemed difficult, but with the right additions it wasn’t too hard.
The main difference is that the zero count needs to be managed in the low-level
rotate queries rather than the higher-level perform-rotation query. This
also makes the perform-rotation query somewhat redundant so the responsibility
to maintain the global state is also shifted into the rotate queries.
The queries missing from below are the same as in the previous part.
%% We may need to increase the zero count %% by more than one. (incf-zeroes $Amount) (zeroes $Some) ($Some plus $Amount into $More) (now) (zeroes $More) %% Process instructions by splitting into %% direction and number, then performing the %% indicated rotation. (process [$Instruction | $]) (split word $Instruction into [$Direction | $Numbers]) (join words $Numbers into $Amount) (rotate $Amount $Direction) (fail) %% Left rotation. Counts zeroes passed and %% updates global state variable. (rotate $Amount @l) %% Account for any zero-passings that %% happen as part of multi-turn rotations. ($Amount divided by 100 into $Turns) (incf-zeroes $Turns) %% Then perform the actual effect. ($Amount modulo 100 into $Actual) (position $Current) { %% If we are overturning, implement %% as the equivalent right rotation. ($Actual > $Current) %% In case we start on zero, we have %% already counted this zero and we %% should not count it again. { ($Current = 0) (or) (incf-zeroes 1) } (100 minus $Actual into $Opposite) (rotate $Opposite @r) (or) %% If we are not overturning, we can %% subtract directly. ($Current minus $Actual into $Next) (now) (position $Next) %% If we landed on zero, we should %% count it. { ($Next > 0) (or) (incf-zeroes 1) } } %% Right rotation. Counts zeroes passed and %% updates global state variable. (rotate $Amount @r) %% Account for any zero-passings that %% happen as part of multi-turn rotations. ($Amount divided by 100 into $Turns) (incf-zeroes $Turns) %% Then perform the actual effect. ($Amount modulo 100 into $Actual) (position $Current) ($Current plus $Actual into $Result) { ($Result < 100) (now) (position $Result) (or) %% One more overturn detected. %% This also handles the case when %% we land on zero. (incf-zeroes 1) ($Result minus 100 into $Next) (now) (position $Next) }
Despite the weirdnesses around arithmetic in Dialog, this worked surprisingly well. I suppose tomorrow might be too hard to even attempt.
Day 2, 2025
I started by, very reluctantly, implementing big integer arithmetic2 The difference between long integer arithmetic and big integer arithmetic is that big integers are unbounded, whereas long integers are bounded, only at more than a machine integer. by representing big integers as lists of integers up to 10³, i.e. each group of three digits in the decimal representation of the number would be its own list item. The least significant thousands place is stored first to simplify addition-through-recursion.
%% Parse a word into a big integer. (bigint-parse $Num $BigInt) (split word $Num into $Digits) (bigint-parse-internal $Digits [$Type | $Reversed]) (list-reverse $Reversed $BigInt) %% During parsing, this stores an additional piece of information %% at the head of the list, which is whether it has parsed the units, %% tens, or hundreds place of the most significant group of digits yet. %% That information is not needed after parsing, so it gets dropped. (bigint-parse-internal [] [nothing 0]) (bigint-parse-internal [$H | $T] $Data) (bigint-parse-internal $T [$Type $Intermediary | $More]) %% There's surely a better way to accomplish this switch %% statement but this was what I came up with. { ($Type = @hundreds) ($Data = [ones $H | [$Intermediary | $More]]) (or) ($Type = @nothing) ($Data = [ones $H | $More]) (or) ($Type = @ones) ($H times 10 into $Tens) ($Tens plus $Intermediary into $Next) ($Data = [tens $Next | $More]) (or) ($Type = @tens) ($H times 100 into $Hundreds) ($Hundreds plus $Intermediary into $Next) ($Data = [hundreds $Next | $More]) } %% Constructor of big integers from short integers. %% Used in addition to extract the carry position. (bigint-new $Short $BigInt) ($Short < 1000) ($BigInt = [$Short]) (or) ($Short divided by 1000 into $Thousands) ($Short modulo 1000 into $Ones) ($BigInt = [$Ones $Thousands]) %% Start the operation off with zero carry. (bigint-add $A $B $R) (bigint-add-internal $A $B 0 $R) %% If we run out of number, we tack on the carry as the %% most significant group. (bigint-add-internal [] [] $Carry $Result) ($Result = [$Carry]) %% Simplify cases by swapping the places of A and B. %% Commutativity rules! (bigint-add-internal [] $B $Carry $Result) (bigint-add-internal $B [] $Carry $Result) %% If we have no more B remaining, it is zero. (bigint-add-internal [$Ha | $Ta] [] $Carry $Result) (bigint-add-internal [$Ha | $Ta] [0] $Carry $Result) %% Add, extract carry, build new bigint. (bigint-add-internal [$Ha | $Ta] [$Hb | $Tb] $Carry $Result) ($Ha plus $Hb into $Sum) ($Sum plus $Carry into $Total) (bigint-new $Total [$This $Next]) (bigint-add-internal $Ta $Tb $Next $Further) { ($Further = [0]) ($Result = [$This]) (or) ($Result = [$This | $Further]) }
There’s also a hacky routine for printing these big integers. It has some extra bookkeeping to not zero-pad the first group of digits.
(bigint-print $BigInt) (list-reverse $BigInt $Reversed) (bigint-print-internal @first $Reversed) (space) (bigint-print-internal $ []) (bigint-print-internal $Group [$H | $T]) (split word $H into $Digits) { ~($Group = @first) (list-length $Digits 1) 00 (no space) (or) ~($Group = @first) (list-length $Digits 2) 0 (no space) (or) %% Empty case to make query succeed even when no %% zero-padding is needed. } $H (no space) (bigint-print-internal @remaining $T)
Oh and the above depends on two list queries which I’m not sure they exist in Dialog, so they got made.
(list-reverse [] []) (list-reverse [$H | $T] $R) (list-reverse $T $Tr) (append $Tr [$H] $R) (list-length $L $N) (accumulate 1) *($ is one of $L) (into $N)
I also wrote the functions to parse a range as given in the problem input.
(range-parse $Input $Start $Stop) (extract-number $Input $Num $Rest) (bigint-parse $Num $Start) (word-tail $Rest $Further) (bigint-parse $Further $Stop) (word-tail $Word $Tail) (split word $Word into [$ | $Characters]) (join words $Characters into $Tail) %% This fails when it consumes the entire string because then it cannot %% construct the $Rest word from the empty list. That's probably unexpected, but %% also fine for this AoC problem. (extract-number $Input $Num $Rest) (split word $Input into $Characters) (extract-number-internal $Characters $Digits $Further) (join words $Digits into $Num) (join words $Further into $Rest) (extract-number-internal [(number $H) | $T] [$H | $Next] $Further) (extract-number-internal $T $Next $Further) (extract-number-internal $Characters [] $Characters)
These work well when queried in the main query.
(program entry point) (get input $Ranges) *($Range is one of $Ranges) (range-parse $Range $Start $Stop) Parsed out the range (bigint-print $Start) to (bigint-print $Stop) (line) (fail)
which, given the example input, prints
Parsed out the range 11 to 22 Parsed out the range 95 to 115 Parsed out the range 998 to 1012 Parsed out the range 1188511880 to 1188511890 Parsed out the range 222220 to 222224 Parsed out the range 1698522 to 1698528 Parsed out the range 446443 to 446449 Parsed out the range 38593856 to 38593862 Parsed out the range 565653 to 565659 Parsed out the range 824824821 to 824824827 Parsed out the range 2121212118 to 2121212124
The next step would be to either (a) implement division and modulo for big integers, or (b) convert these big integers to words. Either of these approaches would let us detect the repetitions we need to solve the problem.
Unfortunately, (a) sounds difficult, and in the process of (b) I ran into the memory limits of Dialog running on the the Z-machine. And then I caught the flu, and then I still tried despite my brain being a mess, and then I just didn’t feel like it anymore.
Sorry! Maybe you’ll enjoy the next part instead.
Notes from learning Dialog
When I started writing the notes below, I hadn’t tried using Dialog yet. However, I had read the excellent manual and it does a great job of selling the language. As a learning exercise, I bit off one of the problems of the 2024 Advent of Code. Dialog has been described as mind-bending, so maybe you’ll get something out of these notes even if you’re not interested in the Z-machine.
Small integers are the bane of my existence
You may remember that the Z-machine only supports 16-bit integers, and that the puzzle input for the first day of 2024 used numbers sized around 75,000, which do not fit into 16 bits. We implemented long integer arithmetic on our own for Inform 6, which was greatly helped by having the 16-bit integers in the Z-machine, meaning we could comfortably store intermediate values without losing carry bits.
Imagine my shock when I read in the Dialog manual that
All numbers in Dialog are restricted to the range 0–16383 (inclusive).
Granted, this is still enough to implement long integer arithmetic, but! Dialog does also not have any bitwise operators, nor does it support emitting Z-machine opcodes directly. The manual goes on to explain that
developers that require more sophisticated number crunching will have to get creative.
You don’t say.
At the time I was learning the language I didn’t have the level of creativity in me to implement long integer arithmetic under those constraints. So I skipped past the first day, and went straight for the second day.
Basics of Dialog execution
Dialog is different from many other programming languages, so we’ll have to start slow. In Dialog, the subroutine is not the fundamental unit of abstraction. Instead, the query is. The main difference is that whereas a subroutine typically
- Has a name,
- Takes arguments,
- Can perform side effects, and
- Returns a value.
queries instead
- Are identified by words,
- Can unify variables with values in both directions,
- Can perform side effects, and
- Always result in either failure or success.
The latter point is important, because it determines how execution flows in a Dialog program. Failure in Dialog does not mean something bad happened – it is a normal way to stop executing a query.
When a Dialog program starts up, it always starts by evaluating the query called
(program entry point). The (program entry point) query must be defined by
us, and we define a query by writing its name. The simplest way to define the
entry point query would be to compile the following complete program:
(program entry point)
This is a query identified by the words program entry point. Because it has no
query body, it always suceeds. We could make it always fail by giving it a body
like
(program entry point) (1 = 2)
The sub-query (1 = 2) succeeds if one is equal to two, and fails otherwise.
Since one is not equal to two, it will always fail, meaning our program, when
defined like that, will always fail. Since failure is counted as a normal way to
terminate execution, this failure will not be visible to the user – the program
simply stops.
If we write plain text in the body of a query, that is taken to be a print statement query. A print statement query always suceeds. Thus the following program will print a customary message and then suceed.
(program entry point) Hello, world!
If we instead wanted it to fail, we could add a failing query to the end of the
main query body. We used the failing query (1 = 2), before, but a convenient
equivalent is (fail), a query that always fails.
(program entry point) Hello, world! (fail)
This would run the first sub-query, which prints a message, and when the second query fails, the main query would also fail. This highlights a key principle of how Dialog programs run: when a query is evaluated, it evaluates all sub-queries in sequence, and fails when any of them fail.
Reading in a list of numbers
Now, perhaps, we can ask the user for a sequence of numbers.
(program entry point) Please enter a sequence of numbers: (line) (get input $Report) Aha, you are interested in $Report!
The program above defines the main query (program entry point) in terms of
four sub-queries:
- First a print statement to print a prompt.
- Then comes a query called
(line). This is built into the Dialog language and causes a newline to be printed.3 Roughly speaking – Dialog applies some intelligence to things that are printed. - We have the query
(get input $Report). This pauses execution and waits for the user to type stuff. If the user types space-separated numbers, Dialog will create a list of numbers and assign that list into the variable$Report. - Finally, we have another print statement, where we display the list back to the user.4 Dialog applies some debug formatting to be able to print lists.
As long as the user types something at the prompt, none of these queries can fail. That means whenever the program runs, Dialog will evaluate all four sub-queries in turn. The results are predictable:
Please enter a sequence of numbers: 6 8 19 Aha, you are interested in [6 8 19]!
or
Please enter a sequence of numbers: 87 44 hammock Aha, you are interested in [87 44 hammock]!
or
Please enter a sequence of numbers: Aha, you are interested in []!
Dialog does not enforce that the user types numbers, but if the user types numbers, the program will get a list of numbers rather than a list of dictionary words, which is the string-like type in Dialog.
Extracting and querying on the first number
We’ll eventually want to do more complicated processing on the sequence we get from the user, but to take it step by step, let’s determine whether the first number the user typed is greater than three.
We’ll make a new query for this. In Dialog, queries are identified with words,
but they can have variable names interspersed between any of the words. This
means we could create a query called (does $List have a large first number?)
where the variable $List representing the parameter comes early on in the
definition. Here’s the full definition of such a query.
(does $List have a large first number?) ($List = [$First | $Rest]) ($First > 3)
In order for this query to succeed, both of its subqueries must succeed. In other words, it must be able to
- decompose
$Listinto a$Firstelement and the$Restof the list; and - the
$Firstelement must be a number greater than three.
If either of those two sub-queries fail, then the entire (does $List have a
large first number?) query will fail.
When we insert a call to the large-number query in our main program, it will either allow the main program to continue to execute (if the large-number query succeeds), or halt execution of the main program (if the large-number query fails). Thus, we can use it to conditionally print a message:
(program entry point) Please enter a sequence of numbers: (line) (get input $Report) Aha, you are interested in $Report! (line) (does $Report have a large first number?) The first number is indeed large!
What actually happens when Dialog encounters a call to a query is that it
matches the name of the query word-for-word with all other queries we have
defined. If any of the words in the query are variables (i.e. start with a
dollar sign) and are currently unassigned, Dialog is allowed to assign them a
value to make the query match. This process is called unification. In this
case, the $Report variable is bound to a list, but the $List variable inside
the query is unassigned, so Dialog assigns the value of $Report into $List
to make the query match its definition. Then execution of the sub-query takes
place.
We defined the query using a verbose name to illustrate how Dialog tries to match the name and assigns values to variables to make it match exactly, but we can use shorter query names. I’m a programmer, so I’d prefer to define our query as
(first-large? $List) ($List = [$First | $Rest]) ($First > 3)
using the much shorter, Ruby-inspired name first-large?. We can also decompose
the argument list in the query declaration directly, instead of with a subquery.
It’s the same thing.
(first-large? [$First | $Rest]) ($First > 3)
This will produce the expected effect.
Please enter a sequence of numbers: 5 8 3 Aha, you are interested in [5 8 3]! The first number is indeed large!
and
Please enter a sequence of numbers: 1 8 2 Aha, you are interested in [1 8 2]!
It’s a little annoying that it doesn’t print anything when the first number is small. We’ll see if we can fix that.
Conditional execution
So far, we have only seen queries that succeed if all of their subqueries
succeed. We can get the effect of conditional execution by defining the same
query multiple times. Here, we have split out the printing of the message into a
large-feedback query.
(program entry point) Please enter a sequence of numbers: (line) (get input $Report) Aha, you are interested in $Report! (line) (large-feedback $Report) (large-feedback $List) (first-large? $List) The first number is indeed large! (large-feedback $List) Sorry, the first number seems small. (first-large? [$First | $Rest]) ($First > 3)
You’ll note that there are now two queries with the same declaration. When
there are multiple queries that match, Dialog sets up a choice point before
executing the first matching query. If that query fails, Dialog will roll back
any changes it made during its execution, and execute the next matching query
instead. In our program above, that means it will try the first large-feedback
query, which fails if the first number is small, and then go on to try the
second large-feedback query. The effect is printing different messages whether
the first number is large or small.
If we don’t want to split up this definition, there’s a control flow primitive
in Dialog called (or) which sets up such a choice point inside a query. To use
that, we’d write the large-feedback query with one query body:
(large-feedback $List) (first-large? $List) The first number is indeed large! (or) Sorry, the first number seems small.
Although (or) looks like a query, it’s the syntax for a control flow primitive
in Dialog. When a query is defined with (or), Dialog first executes the
queries above the (or) as normal, but if any of them fail, it restores to the
start of execution of that query, and runs the queries below the (or) instead.
In the above case, this has the desired effect: if the first number is large, it
will get to the relevant print statement, and then the large-feedback
query is considered successful. In contrast, if the first-large? query
fails, the (or) construct will move execution over to its other side, and
print the consolitory message.
The (or) construct is inconvenient in that it considers everything above it
as one branch, and everything below it as another branch. We can limit its
scope with curly braces, as here:
(program entry point) Please enter a sequence of numbers: (line) (get input $Report) Aha, you are interested in $Report! (line) { (first-large? $Report) The first number is indeed large! (or) Sorry, the first number seems small. } (first-large? [$First | $Rest]) ($First > 3)
This kind of conditional execution is so common that Dialog has syntactic sugar for it to make it look more like other programming languages. We could instead have written e.g.
(program entry point) Please enter a sequence of numbers: (line) (get input $Report) Aha, you are interested in $Report! (line) (if) ($Report = [$First | $]) ($First > 3) (then) The first number is indeed large! (else) Sorry, the first number seems small. (endif)
This is desugared to almost the same thing.5 The difference is that further
failures in the (then) branch won’t toss execution over to the (else)
branch. In these notes, I will try to stick to the more primitive constructs
rather than syntax sugar, to communicate more clearly that what happens under
the hood are queries executing until one fails, then Dialog backtracks and tries
the other alternative.
Querying multiple numbers
Now that we have that in place, let’s construct a more complicated query on the report. Instead of seeing whether the first number is large, let’s see if the entire sequence is increasing. Our query that checks if a sequence is increasing will do so by running another query that checks if the rest of the sequence is increasing, given the first number.
(sequence-increasing? [$First | $Rest]) (sequence-increasing-from? $Rest $First) (sequence-increasing-from? [$First | $Rest] $Previous) ($First > $Previous) (sequence-increasing-from? $Rest $First)
If we plop this down in our main query instead of the first-large? query,
we’ll find that it judges all sequences as not increasing. The reason is that
given the input, say, [4, 6, 9], it will eventually try to run the query
(sequence-increasing-from? [] 9)
and this does not match any queries we have defined, so it fails.6 The functional programmers in the crowd will recognise this as recursion with a missing base case. And when it fails, the failure falls through to the top level. We can easily fix this by defining that query to always be a success:
(sequence-increasing? [$First | $Rest]) (sequence-increasing-from? $Rest $First) (sequence-increasing-from? [] $) (sequence-increasing-from? [$First | $Rest] $Previous) ($First > $Previous) (sequence-increasing-from? $Rest $First)
The empty dollar sign is a wildcard variable, indicating we want the query to succeed for all values of that variable. With this addition, the program works. It does judge the empty input as “not increasing” but that’s probably fine.
Conditional execution based on symbols
To solve the second day’s Advent of Code problem, we will also want to check if a sequence is decreasing. We could copy-paste the above queries and update the operator used, but we could also think of it as an opportunity for abstraction. First, we modify the queries to take a parameter that indicates the criterion on which the sequence should be evaluated.
(sequence-satisfies? $Condition [$First | $Rest]) (sequence-satisfies-from? $Condition $Rest $First) (sequence-satisfies-from? $Condition [] $) (sequence-satisfies-from? $Condition [$First | $Rest] $Previous) (compare-by $Condition $First $Previous) (sequence-satisfies-from? $Condition $Rest $First)
What we have done here is add another variable which indicates by which
condition the sequence should be evaluated. We have also changed the explicit
comparison ($First > $Previous) to a compare-by query that takes a condition
and two values and compares by that condition.
When we call this query, we need to pass it a value indicating by which
condition we want to compare. We could pass it a plain number (and decide on a
convention that e.g. 0 means increasing, 1 means decreasing, etc.) but it’s
more readable if we use a symbol.7 Actually, they are not called symbols in
Dialog, but objects. That seems very confusing to me. They have no properties
and no methods! They are plain symbols. I’m sure the compiler translates them to
a number, too. These are words prefixed by hash signs. Thus, the call to the
new query would look like:
{
(sequence-satisfies? #increasing $Report)
Yup, increasing.
(or)
Sorry, not increasing.
}
In order for this to work, we also have to define the compare-by query, so
we’ll do that.
(compare-by #increasing $A $B) ($A > $B)
With this, we get the expected result.
To support checking if a sequence is decreasing, we only have to implement that
compare-by operation.
(compare-by #decreasing $A $B) ($A < $B)
We can also add the compare-by operation to determine whether a sequence only
consists of small steps.
(compare-by #small-stepped $A $B) ($A < $B) (compare-by #small-stepped $B $A) (compare-by #small-stepped $A $B) ($A minus $B into $Difference) ($Difference < 4)
This looks weird because Dialog isn’t very strong at maths. Dialog doesn’t have
negative numbers, so we have to make sure we are subtracting in the right order.
If $A is smaller than $B, we swap them before performing the computation.
Evaluating the safety of a sequence
The Advent of Code puzzle asks us to determine which reports are safe, where safety is determined by the sequence being small-stepped and being either increasing or decreasing. We have what we need to determine that for a single sequence now.
(program entry point) Please enter a sequence of numbers: (line) (get input $Report) { (report-safe? $Report) The report is safe. (or) Sorry, that report is unsafe. } (report-safe? $Report) (sequence-satisfies? #small-stepped $Report) (sequence-monotonic? $Report) (sequence-monotonic? $Report) (sequence-satisfies? #increasing $Report) (sequence-monotonic? $Report) (sequence-satisfies? #decreasing $Report)
We have a separate query for determining whether the report is increasing or
decreasing because we want Dialog to try both. But we can apply a trick to
simplify the code. We know that Dialog creates a choice point if we have a block
with an (or) query, so we can use that to try both directions.
(report-safe? $Report) (sequence-satisfies? #small-stepped $Report) { ($Direction = #increasing) (or) ($Direction = #decreasing) } (sequence-satisfies? $Direction $Report)
When Dialog runs this query, it will first assign #increasing to $Direction,
because it has not yet seen anything that contradicts this choice. It will then
go through the sequence-satisfies? #increasing query. If that query fails,
Dialog takes that to mean that there is evidence that contradicts $Direction
being #increasing. So Dialog rolls back to the choice point created by the
(or) construct, and runs the rest of the query again except with $Direction
set to #decreasing. If that also fails, then the entire report-safe? query
fails. But if it succeeds, the report-safe? query also succeeds.
This way of writing it would be unnecessarily verbose if we had many directions.
There is a query built in to Dialog called ($ is one of $). That query
succeeds if the first argument is a member of the list in the second argument.
If the first argument is an unbound variable, then Dialog will do its thing
where it assigns a value to the first argument to make the query succeed. In
practice, that means Dialog assigns one of the values of the list to the variable.
We might imagine we could use this query as such:
(report-safe? $Report) (sequence-satisfies? #small-stepped $Report) ($Direction is one of [#increasing #decreasing]) (sequence-satisfies? $Direction $Report)
but this would not work as we want it to. The problem is that Dialog does not
insert a choice point before the is one of query, so it will never be able to backtrack
to try multiple assignments for the $Direction variable. There is a special
little syntax we can use to get Dialog to insert a choice point before that
query. See if you spot it!
(report-safe? $Report) (sequence-satisfies? #small-stepped $Report) *($Direction is one of [#increasing #decreasing]) (sequence-satisfies? $Direction $Report)
The asterisk turns the query into a multi-query, which means Dialog creates a choice point before running the query, and then if any of the subsequent queries fail, it backtracks and tries a new way to unify the query. If the subsequent queries succeed, the overall query evaluation succeeds and the choice point is discarded.
Reading multiple reports
At this point, we can use tail recursion to evaluate multiple reports.
(program entry point) (read-and-evaluate-reports) (read-and-evaluate-reports) (get input $Report) (if) ~($Report = []) (then) (evaluate-safety $Report) (read-and-evaluate-reports) (endif) (evaluate-safety $Report) (report-safe? $Report) The report $Report is safe. (line) (or) Sorry, the report $Report is unsafe. (line)
If we run this with the test input in the second day’s problem, it answers correctly.
The report [7 6 4 2 1] is safe. Sorry, the report [1 2 7 8 9] is unsafe. Sorry, the report [9 7 6 2 1] is unsafe. Sorry, the report [1 3 2 4 5] is unsafe. Sorry, the report [8 6 4 4 1] is unsafe. The report [1 3 6 7 9] is safe.
However, if we run this on the full input, it just … stops after a while, as if
the read-and-evaluate-reports query fails – and this shouldn’t be possible
given how it’s written!
Hidden in the last chapter of the Dialog manual, under the section Some notes on performance, subsection Memory footprint, we find the remark
Tail-call optimization is backend-dependent. This means that infinite loops must be implemented using
(repeat forever). If they are implemented using recursion, some memory might leak with every iteration, and the program will eventually crash.
The problem seems to be that we run out of memory!8 We can install an error
handler in our Dialog code that receives runtime error codes. This confirms the
memory problem. As the manual alludes to, there is a special query built into
the language which always succeeds, and it’s called (repeat forever). The way
we normally use it is as a multi-query, meaning it will be re-tried over and
over as long as any of the subsequent queries fail. Here it is in context.
(read-and-evaluate-reports) *(repeat forever) (get input $Report) { ($Report = []) (or) (evaluate-safety $Report) (fail) }
We need the explicit failure call at the end, because Dialog will only backtrack to the choice point established by the multi-query if the subsequent queries fail. When the input is empty, we don’t fail the query, because then we do want to break out of the query successfully.
Counting safe reports
However, having turned the iteration into an actual loop, we can not explicitly accumulate a count of safe reports, which would have been my initial plan. What we’ll do instead is use a global variable to keep track of that.
(global variable (safe-reports 0)) (read-and-evaluate-reports $Count) *(repeat forever) (get input $Report) { ($Report = []) (safe-reports $Count) (or) (report-safe? $Report) (safe-reports $Previous) ($Previous plus 1 into $Next) (now) (safe-reports $Next) (fail) }
A global variable is like a query definition except that it is allowed to change
when the program runs. At the program start, we have a query defined as
(safe-reports 0). When the program queries for e.g. (safe-reports
$Previous), and the $Previous variable is unassigned, it will assign 0 to
$Previous in order to unify the queries – typical Dialog evaluation. This is
how we can read global variables.
Because we have declared that query as a global variable, we are also allowed to
run something like (now) (safe-reports 5). This will re-define the query to
(safe-reports 5), rather than (safe-reports 0). The (now) directive is
what allows us to replace query definitions during program run-time. We use this
in the (read-and-evaluate-reports) query to increment the count of safe
reports seen.
Running this on the full test input, it actually gives us the correct answer!9 Just like with Prolog, I’m always a little surprised when a Dialog program Just Works™.
Evaluating reports with a number excluded
The twist for the second half of the second day’s problem is that we should consider reports safe also if we can produce a safe report by excluding one of the numbers in the report. This would have taken too much time for me to implement in Inform 6, but it is a natural fit for Dialog!
I can imagine two ways of doing it. We could modify the sequence-satisfies?
query to try skipping over one item at each step, and returning successfully if
any of those attempts are successful. If we want to keep our
sequence-satisfies? unchanged, we could instead first generate all
equivalent sequences (i.e. sequences missing one item), and then test all of
those for safety.
I selected the latter approach. This is a query that generates all equivalent reports, i.e. it takes a report and gives back all reports that result from excluding one number from it.
(report-equivalent [$First | $Rest] $Result) *(report-equivalent $Rest $More) ($Result = [$First | $More]) (or) ($Result = $Rest)
I wrote this. It was actually easy to write. I just typed out the things I felt should be there. But I cannot for the life of me explain how it works.10 This happens when I write Prolog code too. My intuition for it is that we get all equivalent reports constructed from the rest of the numbers in this report and tack on the first number to them. Then when we have exhausted those, we also return the equivalent report we get from excluding the first number.
We can test this by creating a temporary entry point query that overrides the real one.
(program entry point) (get input $Report) *(report-equivalent $Report $Equivalent) We got an equivalent $Equivalent (line) (fail)
The multi-query continues to extract equivalents as long as the main query fails,
which is always does thanks to the (fail) call. When given the input 1 2 3 4
this will print
We got an equivalent [1 2 3] We got an equivalent [1 2 4] We got an equivalent [1 3 4] We got an equivalent [2 3 4]
That’s exactly what we want! We’ll add a new query
(report-with-equivalents-safe? $Report) (report-safe? $Report) (or) *(report-equivalent $Report $Equivalent) (report-safe? $Equivalent)
which, if the report is not safe on its own, tests the safety of all equivalent
reports. We call this in the main query instead of directly calling
report-safe?. When fed the full input, it again returns the correct answer!
Next steps
The flu and other commitments take over here, so this is where this adventure ends.
But I really like Dialog. In this article, we’ve flirted with using it in ways it’s not really meant for. I suspect it can be very powerful for making text adventures, which is what it’s really designed for. But I also find myself wishing Dialog was a better general-purpose language, because it has been nice to work in. Then again, maybe this sharp focus on text adventures is what has kept it nice.