Dialog

Support for cyrillic isn’t implemented yet, but I’ll get right to it. Now that I know that somebody would like to use it, this feature has a much higher priority, so thanks for posting!

1 Like

This response comes a bit late, but you bring up so many interesting points that it has taken me a while to digest everything. First of all, of course, thank you very much for the delightfully specific praise! These design decisions are the result of much deliberation and iteration, and the language went through a lot of drastic transformations before going public. But none of that is described in the manual, which of course tries to describe what the language is, rather than what it isn’t. So it’s very satisfying when someone “gets it”, and provides accurate context for these design decisions in their own words.

This is an interesting idea, and not necessarily all that expensive to implement. The compiler could figure out statically which now-statements might get invoked during such a “dry run”, and add the necessary conditional instructions. But, well, this is not a feature to add carelessly. If it is possible to disable all side-effects dynamically, it might become tempting to put side-effects all over the library code, and expect the caller to be careful. I think that might lead to code that is harder to read and debug. But it’s an intriguing idea, and I’ll probably keep it at the back of my mind.

Closures provide some of the functionality of passing around prolog terms, but it’s not possible to build source-code structures from scratch, as data, and then execute them. That would have to be done using lists somehow. But I’d like to see a concrete example of where it would be useful in a story authoring context, before complicating the language any further.

This is a good point. Certainly, well-written I7 code (at least when it deals with concrete objects in the game world) can convey the purpose of the code to the reader in a very efficient, natural manner, even if the reader is new to the language. That is one of the big strengths of I7. But consider the question: Why are we reading the source code? Sometimes, indeed, we do it in order to glean the original intent of the programmer. But quite often, we read code because we’re trying to understand why it doesn’t work, or why it doesn’t behave in the way that we expected. In those situations, I find that it is immensely helpful to be able to see what the code actually does instead of what the programmer thought it would do. And the more we learn about what the programmer intended, the more we tend to see the code through the eyes of the programmer, and therefore miss the bug for the same reason that the programmer missed the bug. I’m not trying to say that we should design programming languages to be cryptic. Clarity is vital, but it should be clarity about what the program actually does, formally, when you run it, rather than clarity about what the programmer was attempting to do. In my personal, possibly controversial opinion.

This is a good point. Looking at @natecull’s subsequent suggestion, I don’t see a need to put predicates into lists. One could instead do:

(public (my public predicate $))
(public (my other public predicate $))

Or one could make everything public by default, and have a private meta-declaration. But perhaps an even better approach would be to reserve a special syntax for private predicate names, e.g. a colon as the very first character. Such predicate names would be treated as local within the current source code file. Something like:

(my public predicate $X into $Y)
	(:my private predicate $X accumulating [] into $Y)

Another file could then use (:my private predicate $ accumulating $ into $), and it would be a different predicate. Cluttering predicate names with special characters may look bad, but the benefit is that the information is present where it needs to be, at the call site, and there’s little risk of forgetting to add a public/private declaration elsewhere in the file. Well, these are just idle musings for now, but perhaps in the future…

The syntax for closures went through a lot of revisions before I settled on the current one. In fact, I’ve considered several of your suggestions. But consider this:

What you do instead, right now, is this:

	(query { ($_ = [$A $B]) (do something to $A giving $C) (something else $C $B)} [#foo #bar])

I think the latter is quite readable as it is. Though, by all means, you could define the following helper predicate:

(query $Closure $X $Y)
	(query $Closure [$X $Y])

Perhaps I misunderstand you here, but it is certainly possible to put closures in dynamic predicates, because closures are just lists. However, all variables must be bound.

Hmm. Perhaps. It takes the language in an object-oriented direction, and I’m quite happy to be rid of that baggage. The feature is already there, in a sense, but the syntax is cumbersome as you’ve noted. And that will dissuade programmers from thinking in terms of black-box objects and classes, and encourage white-box modelling with rules.

2 Likes

@lft is there a plan to release the source of Tethered as a larger example project for Dialog?

On a related note, I’d like to also voice my support for publicly hosted source on GitHub/Gitlab. No rush, of course.

2 Likes

Hi Linus. Thanks very much for your replies!

Yes, I realised after I wrote that (and with some experimenting) that we can indeed put closures into lists, so we can put them anywhere, and I forgot that you’d mentioned in the manual that we can use lists to pass multiple parameters to a closure’s single $_. So I’m going to go and try to see if I can implement map and reduce/fold using that technique, and see if it makes my core loop in Planner any clearer to read…

Some of my other ideas aren’t really about Dialog as such or the use-case of interactive fiction, but for potentially modernising and cleaning-up Prolog as a desktop or web-scale programming language. I agree that it’s really not a good idea to put nested predicates into Dialog - it clutters the language and makes it harder to tell what’s compile-time and what’s runtime. At the desktop or web scale, however, I think having ‘typed’ expressions (in a very loose sense) like classical Prolog’s terms, which are just slightly more structure than lists, could be very helpful. But this is way out of scope for the Z-machine.

I think a lot of your instincts (eg an unhappiness with the object-oriented black-box approach vs preferring white-box rules) are very much the same as mine.

I like the idea of implementing modules by marking ‘private’ predicates! That seems like quite an elegant solution.

The idea about (collect changes) is probably also something best ignored for the Z-machine, and more my blue-sky thinking about how a completely unrelated ‘pure functional Prolog’ at desktop or web-scale might possibly leverage some of Dialog’s design decisions. Such a Prolog wouldn’t have any side effects at all, maybe… the evaluation of a predicate might just produce a data stream which would either be ignored, or captured.

yay, and here’s map:

(map $Transform over $ListA into $ListB)
	(collect $B)
		*($A is one of $ListA)
		(query $Transform [$A $B])
	(into $ListB)

called via eg:

  (map {($_ = [$A $B]) ($A times 2 into $B) } over [1 2 3] into $ListB)

Edit: also filter:

(filter $Filter over $ListA into $ListB)
	(collect $A)
		*($A is one of $ListA)
		(query $Filter $A)
	(into $ListB)
	(filter {($_ modulo 2 into 0)} over [1 2 3 4] into $BarX)

I wonder if this improves readability?

(agenda $A contains intention $P)
	(filter {($_ is an intention)} over $A into [$P|$])

vs

(agenda [$P|$] contains intention $P)
	(purpose $P is an intention)
	
(agenda [$|$Ps] contains intention $P)
	(agenda $Ps contains intention $P)

However, dgdebug doesn’t seem happy calling such a higher-order query interactively:

> (1 plus 1 into $X)
Query succeeded: (1 plus 1 into 2)
> (map {($_=[$A $B]) ($A plus 1 into $B)} over [1] into $X)
dgdebug: parse.c:932: parse_expr: Assertion `cl == predname->pred->clauses[id]' failed.
Aborted (core dumped)

But if an identical line exists in a rule, I get a different result:

(intro) rule includes:

 (map {($_=[$FooA $FooB]) ($FooA plus 1 into $FooB)} over [1] into $FooX)
  (par) $FooX

result output:
[2]

but then interactively:

> (map {($_=[$FooA $FooB]) ($FooA plus 1 into $FooB)} over [1] into $FooX)
Query succeeded: (map [0] over [1] into [])
>

Hmm. Trying to naively implement ‘reduce’, of course doesn’t work because a Prolog closure doesn’t work like a Lisp closure; the variables get bound and can’t be unbound. So the recursive loop succeeds once then fails on the second iteration.

(reduce $ from $Initial [] into $Initial)
  (reduce $Reduction from $Initial [$First|$Rest] into $Final)
  (query $Reduction [$Initial $First $Result])
  (reduce $Reduction from $Result $Rest into $Final)

This makes closures a bit less useful than I thought… it seems they can only be used once, or with backtracking, ie, with (collect… into). Is there a good way of implementing reduce through backtracking? I don’t see any obvious way of passing the accumulated value through interations…

We don’t get this problem with classical Prolog higher-order predicates because the ‘predicate’ passed is only an atom which is the predicate name, so there are no variables to get bound.

I guess there’s nothing really complex about just doing reduction loops the old-fashioned Prolog way, with hand-crafted first-order predicates and accumulators.

I haven’t decided yet. The code is messy, and doesn’t exactly showcase the language in a good way.

But I’m working on my next game, and this time I try to keep the source code neat and structured, with the intention of releasing it as a large-scale example once it is finished.

This will probably happen soon, as the number of pending issues is growing.

2 Likes

You’re right, a proper example would be better for documentation.

Sounds good.

Here is Dialog release 0g/04, library 0.27.

  • Å-machine backend and dgdebug: Improved support for Unicode characters, including case conversion.

  • Å-machine backend: Fixed a bug in the string encoding, where certain international characters ended up as “??”. This bug was reported by @cheshire. The reason why a seemingly random subset of the cyrillic alphabet was acting up was that the bug didn’t affect the first 32 non-ASCII characters encountered in the source code.

  • Rephrased “Predicate … is invoked but there is no matching rule” as “A query is made to …, but there is no matching rule definition” in the general case, and then removed the warning altogether for (error $ entry point). Added some minor clarifications to the manual. These problems were reported by @Jeff_Nyman.

  • Library: Added a new ‘(game over option)’ predicate, for adding custom options to the game over menu.

6 Likes

Here is a download link for Dialog 0g/05, library 0.28.

Library feature: Heads of noun phrases

To assist with disambiguation, it is now possible to declare certain words to be potential heads of the noun phrase for a given object. The head of a noun phrase is the main noun, such as “cap” in “the bright red bottle cap of doom”.

Thus, we might define:

#bottle
(name *)        red bottle
(dict *)        crimson decanter
(heads *)       bottle decanter

#cap
(name *)        red bottle cap
(heads *)       cap

Now, if the player types EXAMINE BOTTLE, this is unambiguously interpreted as a request to examine the bottle, not the bottle cap, because one of the heads of #bottle was given. If the player types EXAMINE RED, the game will ask if they wanted to examine the red bottle or the red bottle cap. In response to that, the answer BOTTLE is unambiguously understood as the #bottle.

The list of noun heads is only consulted to resolve ambiguities. If the player attempts to TAKE BOTTLE while holding the bottle but not the cap, for instance, then that is interpreted as a request to take the bottle cap.

Hence, authors may add (heads $) definitions as needed, on a case-by-case basis, when ambiguities turn up during playtesting.

Compiler features

  • Å-machine backend: Now obeys the -s option for stripping away internal object names (hash tags) from the final story file.

  • Compiler: The quadruple-verbose output (-vvvv) now includes a list of all words the story might print. With a bit of scripting, these can be sent to an external spell checker.

  • Debugger: Commandline option -L to disable hyperlinks in the output. This also affects (interpreter supports links).

Bugfixes

  • Compiler: Fixed a bug where (determine object $) didn’t accept integers in the input.

  • Library: If parsing fails when default actions are enabled, don’t assume that a default action was intended. Thus, “enterr car” no longer results in “I only understood you as far as wanting to examine something” (when examine is the default verb).

  • Compiler: Fixed several corner-case bugs discovered through fuzzing.

5 Likes

Dialog 0g/06, library 0.29 contains some small but significant improvements:

  • Library: In the before-rules for eating and drinking, only attempt to pick up the indicated object if it is edible or potable.
  • Library: Treat “X, tell me about Y” as “ask X about Y”.
  • Library: Fixed a bug where (describe topic $) couldn’t deal with ambiguity.
  • Compiler: Fixed a bug related to the optimization of nested disjunctions.

The compiler bug was particularly elusive and would only trigger under very specific conditions. But it could result in code that would crash the game in certain corner cases, so make sure to upgrade.

3 Likes

In case you ever make any progress with your emacs-mode for dialog, let me know. It’s been very helpful so far!

1 Like

Yay! Congrats for the Best Technological Development!

4 Likes

Huge congrats! Incredibly cool.

4 Likes

I have created a mostly-complete conversion of Pick Up the Phone Booth and Die to Dialog.

…enjoy?

Improvements welcome.

3 Likes

Incidentally, is there an official/accepted way to disassemble a Dialog Z-Machine binary? putpbad.z5 seems to break txd’s tiny brain, generating a bunch of errors and >100 MB of output. (OSX build of txd from ztools731)

Txd works, but you have to use -g to stop it from trying to analyze the grammar table. That’s because in Dialog, grammar is just ordinary code.
Dialog Z-code is highly optimized and tricky to follow. Text strings are useful landmarks. Check out zcode.h to learn what the global variables do. You can also run dialogc in quadruple-verbose mode (-vvvv) to see the intermediate code, which is fairly close to the final Z-code, but better annotated. The routines end up in a different order, though.
Happy hacking!

2 Likes

‘but this one has the words “PHONE BOOTH” scratched out and “P#0N3 B00TH” instead.’ …

I didn’t notice until watching my own video playback that Dialog was understandably confused about the # in p#on3.

Escaping it in the (descr) as

P\#0N3

works fine.
But in the dict, it didn’t help:

(dict *)	p\#0n3 b00th

b00th is fine, but I still can’t get the game to treat p#on3 as a valid way to refer to the booth.

Is there a solution, other than “don’t do this”? :slight_smile:

(The video is at https://www.youtube.com/watch?v=ciEBW61IUPo , for the curious.)

Hi!

This sounds like a bug, but I’m not able to reproduce it. Here, “p\#0n3” works correctly on all compiler backends. In your message, you’ve spelt it with a zero in one place, and the letter o in another. Could that be the root cause, or is it just a typo in the message?

I enjoyed watching the video! It allowed me to see the language through a beginner’s eyes, which was cool and also provided useful feedback.

It seems I have to clarify the role of (notice $) in the manual. Notice has nothing to do with the appearance of an object, and defining a rule for (appearance $ $ $) is sufficient to make the object stand out in the room description. Notice binds a pronoun to the object. In this case, because the phone booth is neither (male $), (female $), nor (plural $), the pronoun “it” is bound to it. Hence, the player can type “pick it up” and die.

Instead of the (victory lap $) thing, I would use select/stopping, like this:

(on every tick in #townsquare)
	(booth has been pushed)
	(select)
	(or)
		(par)
		A familiar sound ...
		(game over {You have won})
	(stopping)

On line 207, visible at 14:15, it looks like you have a bug: The condition ($Max) invokes the predicate ($), which is used for hyperlinks. The original code from the library queries (current score $Max), which is something else. You can probably remove this condition altogether, since you already know that a maximum score is defined.

Thanks for taking an interest in Dialog, and for making a nice video!

(I am embarrassed to report that it was in fact me not correctly entering “p#0n3” in-game. Moving right along…)

Thank you for the illustration of how select/or/stopping can be used for this kind of simple counter! Much cleaner.

I’ve cleared out the bad/unnecessary $Max code, and will work on comprehending better what you said about why exactly that condition was not just unnecessary (because, as you say, I as the author know there’s a maximum score) but why I was also using $Max incorrectly.

Thanks for the correction on (notice $), I wish I could explain better why I felt it was necessary.

Speaking of properly defining the #booth:

What would be the Dialog way to replicate this behavior from the Inform original?

[game starts, first move entered is]
>take
(the phone booth)
You grunt with all your might and heave [etc]
    *** You have died ***

My Dialog version produces

> take
(I only understood you as far as wanting to take something.)

Looking at trace info in the debugger produces a lot of things I don’t understand! #booth is mentioned as being in scope and visible but obviously isn’t being chosen.

I don’t know what the Inform code was doing. In ZILF, the TAKEBIT flag is enough to make that maneuver work.

Is it just a design characteristic of the Dialog (and/or the stdlib) that a simple TAKE isn’t expected to work? Or is there a simple way of enabling this behavior that I’ve missed?

1 Like