Dialog Wishlist

What I’ve discovered while poking at this: it actually already exists in the compiler. It just doesn’t have a name you can type. It’s internally called (*first result*).

It’s needed because of access predicates. If you modify my example to use an access predicate:

(useful action)
	(droppable item in inventory and supporter in room)
	PUT something ON something

@(droppable item in inventory and supporter in room)
	*($Item is #heldby #player) ~($Item = #compass)
	(current room $Room) *($Obj is #in $Room) (supporter $Obj)

This is compiled into:

(useful action)
	(*first result*) {
		*($Item is #heldby #player) ~($Item = #compass)
		(current room $Room) *($Obj is #in $Room) (supporter $Obj)
	}
	PUT something ON something

So what I’m proposing actually requires basically no change to the underlying compiler. I’m just giving it an author-facing name. That makes me feel even better about this.

4 Likes

That was…shockingly easy to implement. Like ten minutes of work. Writing the documentation took longer than adding the syntax. That’s what happens when I just have to give a new name to something Linus has already built, I guess!

As usual, I’ll leave the pull request open for a week or two so people can raise objections. But feel free to check out the new build and see how it works.

New test case
#alpha
(cond *)

#beta
(cond *)

(program entry point)
	(exhaust) { *(test case) (line) }

(test case)
	*(object $Obj) (cond $Obj)
	Inline

(test case)
	(access)
	Access

@(access)
	*(object $Obj) (cond $Obj)

(test case)
	(predicate)
	Predicate

(predicate)
	*(object $Obj) (cond $Obj)

(test case)
	(at most once) { *(object $Obj) (cond $Obj) }
	At most once

Output:

Inline
Inline
Access
Predicate
At most once

New manual section

[#at-most-once]
== Visiting fewer solutions

(exhaust) is a powerful tool: it will explore every branch from every choice
point, whether you want it to or not. For example:

[source]

(fruit #apple)
(fruit #tree)

(red #apple)
(red #rose)

(leafy #rose)
(leafy #tree)

(portable #apple)
(portable #rose)

(program entry point)
	(exhaust) *(explore the properties)

(explore the properties)
	*(fruit $Obj) (portable $Obj)
	There is a portable fruit!

(explore the properties)
	*(leafy $Obj) (portable $Obj)
	There is a portable leafy thing!

(explore the properties)
	*(red $Obj) (portable $Obj)
	There is a portable red thing!

The output is:

[.output.matches-previous]

There is a portable fruit!
There is a portable leafy thing!
There is a portable red thing!
There is a portable red thing!

Since there are two portable red things, the (exhaust) explores both options,
and prints the message for both.

If we only want a single message here, we can prefix our search with
(at most once). As soon as the inner statement succeeds, all choice points
created since the (at most once) will be discarded.

[source]

(fruit #apple)

(red #apple)
(red #rose)

(leafy #rose)
(leafy #tree)

(portable #apple)
(portable #rose)

(program entry point)
	(exhaust) *(explore the properties)

(explore the properties)
	(at most once) { *(fruit $Obj) (portable $Obj) }
	There is a portable fruit!

(explore the properties)
	(at most once) { *(leafy $Obj) (portable $Obj) }
	There is a portable leafy thing!

(explore the properties)
	(at most once) { *(red $Obj) (portable $Obj) }
	There is a portable red thing!

The output now becomes:

[.output.matches-previous]

There is a portable fruit!
There is a portable leafy thing!
There is a portable red thing!
2 Likes

This might be useful for dialog-extension’s tutorial and conversation systems as well, possibly.

1 Like

Also in 1c/01 (I hope): the manual chapter on testing that I didn’t get done in time for 1b/01. I’m planning to include unit.dg tests, integration-style testing with regtest.py, and perhaps how to pull all that together with a Makefile. Should I also include a bit about Skein testing with dgt, or is dgt separate enough to just stay as its own thing?

1 Like

Can we talk about in-seats versus on-seats? My sense is that the number of seats that get implemented for which there are separate meanings for sitting inside versus on top is relatively small. And to that, I’d suggest that there’s also such a thing as an “at-seat”, for cases such as SIT AT THE CONTROLS or SIT AT THE ENGINEERING STATION. And maybe there’s even a “by-seat?”

It feels like we’ve introduced “guess the preposition.” I get that this all came out of containers vs. supporters, but is there some way to unify this, and either accept all the prepositions for any seat, or have a flexible way to specify which ones make sense?

ETA: And now that I think about it more, the current situation is kind of bonkers. One properly sits in a chair, but on a stool, and both of those are properly modelled as actor supporters, not as containers. Currently, stdlib has you handle them differently, which is nuts.

1 Like

I think the intent is that the player can use any preposition for any seat, but “on-seat” vs “in-seat” specifies what preposition the library uses when describing it. So you get “Living room (in the chair)” but “Living room (on the couch)”.

If this isn’t currently the case, then it definitely should be.

Again, whether it’s an actor supporter or an actor container isn’t the right gating factor here. “Bridge (at the helm)”, “Bridge (in the captain’s chair)”, and “Crew Lounge (on a barstool)” should all work, despite them being all actor supporters.

I see the logic there, but I don’t think Dialog has any facility to set the preposition separately from the relation (#on, #in, etc). It might be a good idea to add one, but I’m not sure how best to make that work without interfering with the usual object tree system.

I fully agree that the on-seat and in-seat distinction isn’t a very good one as currently implemented; I’m just not sure how to make it better either.

3 Likes

Has this ever happened to you?

(defined but not queried)
	Useless stuff here

(program entry point)
	(if) (queried but not defined) (then)
		Huh??
	(else)
		Good!
	(endif)

Warning: No library (such as stdlib.dg) was specified on the commandline.
Warning: queried_not_defined_warning.dg, line 1: Possible typo: A rule is defined for ‘(defined but not queried)’, but this predicate is never queried and has no interface definition.
Warning: Possible typo: A query is made to ‘(queried but not defined)’, but there is no matching rule or interface definition.

We get a line number for the (defined but not queried) warning, but no line number for the (queried but not defined) warning.

Why is that? This block seems like it should be reporting line numbers…

defined = (pred->flags & (PREDF_DEFINED | PREDF_DYNAMIC | PREDF_MACRO));
queried = pred->flags & PREDF_MENTIONED_IN_QUERY;
interface = !!pred->iface_decl;

if((pred->flags & PREDF_DEFINED) && !queried && !interface) {
	assert(pred->nclause);
	report(
		LVL_WARN,
		pred->clauses[0]->line,
		"Possible typo: A rule is defined "
			"for '%s', but this "
			"predicate is never queried "
			"and has no interface "
			"definition.",
		predname->printed_name);
} else if((pred->flags & PREDF_DYNAMIC) && !queried) {
	report(
		LVL_WARN,
		0,
		"Possible typo: '%s' appears in a "
			"now-expression, but is "
			"never queried.",
		predname->printed_name);
} else if(queried && !defined && !interface) {
	report(
		LVL_WARN,
		pred->invoked_at_line,
		"Possible typo: A query is made to "
			"'%s', but there is "
			"no matching rule or "
			"interface definition.",
		predname->printed_name);
} else if(interface && !queried && !defined) {
	report(
		LVL_WARN,
		pred->iface_decl->line,
		"Possible typo: An interface is "
			"defined for '%s', but there "
			"is no matching rule "
			"definition or query.",
		predname->printed_name);
}

…but it turns out pred->invoked_at_line is only set when invocations are traced, which is after this block happens. Oops.

I made it so pred->invoked_at_line is set at the same place PREDF_MENTIONED_IN_QUERY is, so now we get:

Warning: No library (such as stdlib.dg) was specified on the commandline.
Warning: queried_not_defined_warning.dg, line 1: Possible typo: A rule is defined for ‘(defined but not queried)’, but this predicate is never queried and has no interface definition.
Warning: queried_not_defined_warning.dg, line 5: Possible typo: A query is made to ‘(queried but not defined)’, but there is no matching rule or interface definition.

Much better! I thought this was going to be a new feature, but it turned out to be an old (as in predating 1a/01) bug instead. This is going to make debugging typos a lot easier for me!

Pull request, as usual; I’m going to merge this one in a day or two unless someone strongly objects. It seems like a very clear bugfix to me.

1 Like

Also, pressing Ctrl-D at a [more] prompt will now close the debugger. There’s code to make this happen, but it was looking for the wrong value—for some reason, Ctrl-D in my terminal returns 4 instead of EOF. Now it accepts both.

I love when I run into an annoyance while authoring and can immediately go fix it.

1 Like

4 makes sense as Ctrl+D, though: the characters A-Z are 0x41 through 0x5a, a-z are 0x61 through 0x7a, and adding control clears the two bits 0x60?

I don’t know off the top of my head what level Ctrl+D is normally handled at: maybe if you have a readline library involved, it probably processes it and ends the input stream…

Yeah, I thought the POSIX keyboard handler intercepted Ctrl-D and closed the stream instead (meaning read(2) would get zero bytes of data instead of a single 4), but apparently not! There is code in the debugger that looks for the stream being closed and exits the program if so, but it wasn’t triggering, so I made it look for a raw value of 4 as well, and now it seems to work.

1 Like

One more bugfix for the day. Previously, the compiler handled constant lists in rule heads in a special way that’s very fast at runtime, but uses O(n) temporary variables. It builds the list from the front, unifying the elements with the query arguments one by one. This is good for making rules like (perform [put $Obj #on $Supporter]) very efficient (it can stop building the list as soon as something doesn’t match), but there are only so many registers available to put temporaries in, so really big lists can exhaust those registers and crash the compiler.

So now, if a constant list in a rule head is sufficiently long (at least 10 elements if the rule is called with unbound parameters, 20 elements if not), it will be compiled the same way as constant lists in rule bodies. This builds the list starting from the end, which means it can’t short-circuit the comparison (comparisons have to start from the front), but it only uses O(1) registers.

See this thread for the symptoms, and this thread for the causes. I don’t expect this to come up often—list literals with 10 or more elements aren’t common—but it causes no harm in the more usual case, and anything that keeps the compiler from crashing is an improvement in my book!

2 Likes

(In theory, it should be possible to reduce the number of temporaries even when building lists from the front: just reuse earlier ones once they’ve served their purpose. This is complicated to do in the general case, though—not just a single list but nested ones—and this solution works well enough for the moment.)

…unless you’re using lookup tables to get around Dialog not having the math functions that you need. Or you’re cramming a whole bunch of state into a list to keep from having lots of per-object variables.

1 Like

I will say, the best way to do a lookup table is probably to generate it in another programming language and save it in its own source file:

(square root of 0 is 0)
(square root of 1 is 1)
(square root of 2 is 1)
(square root of 3 is 1)
(square root of 4 is 2)
(square root of 5 is 2)
.....

Dialog compiles predicates like this into very efficient internal lookup tables that don’t need to use the heap (especially when the first parameter(s) are inputs and the last parameter(s) are outputs). I haven’t profiled it, but I would expect an order of magnitude speedup over traversing a list at runtime.

Of course, this is no help when you’re using the lists to store state, like in the PRNG example. And something being inefficient is no reason for the compiler to crash when encountering it!

2 Likes

Time for some rewriting. :slight_smile: