dialog random closure bug

go into any dialog game (all mine do it and so does “the wise woman’s dog”).

ask for any object, like ASK FOR HUMAN

the game will appropriately respond with (I only understood you as far as wanting to tell someone to give your human to yourself.)

but it will also print out something from a random closure somewhere else in the game. in my games this is usually game over messages.

running a trace, this seems to be (invoke-closure) misbehaving.

i’ve poked around at the code but it’s well above my head.

2 Likes

Oho! Now this is a fascinating one! It seems to be a library bug, not a compiler bug.

The way Dialog('s standard library) parser works, it first tries to parse the words into an action, gathering up all the possibilities into a list. If that doesn’t work, it sets a flag called (allowing parse errors) and runs the parser again, this time taking only the first result. When this flag is set, routines like “look for a noun” are also allowed to return special values like [] to mean “I’m looking for a noun but not finding one”, [1] to mean “I’m looking for an animate noun but not finding one”, [,] to mean “I’m looking for one object but found a list instead”, or [all] to mean “I’m looking for one object but found ALL instead”.

Keep this in mind for later.

When it’s looking for a noun, it calls the predicate (parse object name $Words as $Result $All $Policy). $All is set to 1 if ALL is allowed, and $Policy indicates which objects should be preferred. This can either be a closure like { (item $_) } (to prefer items), or an object (to prefer children of that object). It’s enforced like this:

(interface (verify object policy $Policy $<Obj))

(verify object policy (object $Policy) $Obj)
	($Obj has parent $Policy)

(verify object policy (nonempty $Policy) $Obj)
	(query $Policy $Obj)

Why (nonempty $Policy)? Because closures are internally represented by lists. Something like { ($_ has parent $Obj) } is compiled into something like [43 $Obj], and a rule like this is added at the top of the program:[1]

(query [43 $Obj] $_) ($_ has parent $Obj)

This means there’s no way to tell for sure if something is a closure. You just have to check whether it’s a nonempty list.

So far so good, right?

Now, here’s how ASK X FOR Y is parsed.

(understand [ask | $Words] as [tell $Actor to give $Obj to $Player])
	*(split $Words by [for] into $Left and $Right)
	*(understand $Left as single object $Actor preferably animate)
	*(understand $Right as object $Obj preferably child of $Actor)
	(current player $Player)

What happens if you ASK FOR SOMETHING with no first object?

  • $Words is set to [for something].
  • Splitting this by for produces [] and [something].
  • [] doesn’t match any object. Parsing fails.
  • Let’s try again with (allowing parse errors) set!
  • $Words is set to [for something].
  • Splitting this by for produces [] and [something].
  • [] doesn’t match any object. Since we’re looking for an animate object, and allowing parse errors, return [1] (which means to print “someone” in the error message).
  • Now, try to parse [something] as an object, preferably a child of [1].
  • If we find a matching object, compare it against the policy [1].
  • Since [1] is a nonempty list, we check the policy by calling (query [1] $Obj).

Uh oh! [1] was supposed to be an object placeholder here, not a closure! But it’s a nonempty list, so now we’re calling the second closure compiled, whatever that might be. (The first one is [0 | $Whatever].)


  1. Though actually with an intermediate level called ( invoke-closure $ $ $) between (query $ $) and the new rules. ↩︎

2 Likes

How do we fix it? Well, my first thought is to always use a closure as the policy, rather than an object. Using objects as the policy only happens when you (understand $Words as object $Obj preferably child of $Parent), which is invoked by this ASK X FOR Y rule, and the [child] grammar token, and…nowhere else. And [child] is never actually used in the standard library. So this should actually increase efficiency, because now we don’t have to check whether the closure is a nonempty list before querying it, and using a closure instead of an object as the policy is vastly more common.

I’ll make that change tonight and see if any of the test cases break. I don’t think they should.

Thank you for catching this!

2 Likes

Fixed!

1 Like

Sadly this didn’t end up working. The library seems to use $ for the null policy (accept everything), so we still have to check if the policy is bound to something, so no real efficiency improvements.

But the behavior is a definite improvement, and now we don’t have to check if the policy is an object (which it almost never was), so it’s still better than before!